正则(4)五阶魔方

本文详细介绍了五阶魔方的复原方法,包括魔方的三要素:组成部件、可执行操作和目标态。首先,简单复原中心区,然后通过特定步骤组装侧边区,最后运用类似三阶魔方的技巧完成复原。文章特别强调了在处理特殊情况下如何转换和解决复原难题。

目录

一,魔方三要素

二,复原方法

1,复原中心区

2,组装侧边区

(2.1)棱块互换

(2.2)棱组交叉

(2.3)case1

(2.4)case2

(2.5)case3

3,复原

三,五阶魔方网页版

1,js部分

2,Html部分

3,Html部分bug修复


一,魔方三要素

(1)组成部件

n阶魔方都可以按照三阶魔方的方式进行分区:

三阶魔方中心块对应n阶魔方(n-2)*(n-2)个块组成的中心区。

三阶魔方棱块对应n阶魔方n-2个块组成的侧边区。

三阶魔方8个角块对应n阶魔方8个角块。为了方便,我们把五阶魔方的侧边区的3个棱块,叫做2个侧边区侧棱、1个侧边区中棱。把中心块区的9个块,叫做4个中心区角块、4个中心区棱块、4个中心块

(2)可执行操作

有12种操作,即上下前后左右 各1层或2层顺时针旋转90度。

对于n阶魔方,就有3n-3种操作。

(3)目标态

所有块位置和朝向正确。

二,复原方法

n(n>3)阶魔方都可以用降阶法,先转化成3阶魔方再复原。

1,复原中心区

这个很简单,不需要公式

2,组装侧边区

即把每个侧边区的3个块都组装到一起。

常规操作就是6步:F_{23}'RU'R'UF_{23},术语参考三阶魔方术语

效果相当于把下图复原:

也可以理解成白色箭头画的3个侧棱轮换,其他部件都不动。

组装侧边区最终需要考虑的有5种场景:棱块互换、棱组交叉、三种组合场景

PS:如果一开始做了棱块位置奇偶性处理,单独的棱块互换、单独的棱组交叉这2种情况其实都不会出现。

(2.1)棱块互换

如果把四阶魔方的“单棱翻转”理解成整个棱组朝向统一反了,那五阶魔方不会出现。

但是如果理解成2个棱块之间的位置互换,那五阶魔方也涉及:

这种情况下,要么转化成棱组交叉场景再处理,要么交换2个棱块解决总状态奇偶性问题,交换方法同四阶魔方的“单棱翻转”处理方案。

PS:棱组交叉也是总状态奇偶性有问题。

(2.2)棱组交叉

处理方案同四阶魔方的“单棱翻转”处理方案。

这里有个概念需要澄清。

偶数阶魔方在合并最中间的2个棱块时,即使一开始做了棱块位置奇偶性处理,还是会遇到U位R位棱组交叉。这种情况下棱块位置奇偶性是没问题的。

偶数阶魔方的其他棱块、奇数阶魔方的所有棱块,只要一开始做了棱块位置奇偶性处理,就不会遇到U位R位棱组交叉

本质上,这2种情况只是看起来一样,其实是不一样的。

(2.3)case1

相当于棱块互换+棱块互换的叠加。

只需要合适的3次棱块合并公式即可转化成case3

(2.4)case2

相当于棱组交叉+棱组交叉的叠加。

只需要合适的3次棱块合并公式即可解决。

(2.5)case3

相当于棱块互换+棱组交叉的叠加。

只需要合适的2次棱块合并公式即可解决。

至此,组装侧边区的问题全部解决。

3,复原

和三阶魔方一模一样,不会出现特殊情况。

三,五阶魔方网页版

1,js部分

参考自动运行按钮

2,Html部分

提示词:

做一些简化,取消底面镜子,取消外层转动和双层转动的按钮,保留其他按钮,外层转动和双层转动仍然可以通过操作序列执行按钮去实现,这部分保留。

deepsek输出:。。。。。。略

3,Html部分bug修复

我的本意是多层操作的层数范围是1到N-1,比如四阶魔方R开头的操作有R  R2  R3  R' R2' R3'六种,但是AI实现的是只有单层和双层操作,所以没有R3和R3'操作。

这个我在四阶魔方一直没关系,因为我的复原方法用不上,不涉及。

提示词:

操作集需要扩充,操作层数的范围是1到N-1,比如五阶魔方就支持R R2 R3 R4这些操作,x就等价于R5。执行操作序列或者自动运行时,遇到非法输入,要弹出错误提醒。

不要单层操作,R系列操作就只有R R2 R3 R4 这几种就行了。

deepsek输出:

<!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: 20px 16px 30px;
      overflow: auto;
    }
    h1 { margin: 0 0 4px; font-size: 24px; letter-spacing: 3px; text-align: center; }
    #status {
      min-height: 24px;
      margin: 2px 0 10px;
      font-size: 14px;
      color: var(--dim);
      transition: color .3s;
      text-align: center;
    }
    #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; }
    }

    #layout {
      display: flex;
      gap: 20px;
      width: 100%;
      max-width: 1420px;
      margin: 0 auto;
      align-items: flex-start;
      justify-content: center;
    }
    #leftCol {
      flex: 0 0 420px;
      width: 420px;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    #middleCol {
      flex: 1 1 auto;
      min-width: 340px;
      max-width: 580px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }
    #rightCol {
      flex: 0 0 330px;
      width: 330px;
      position: sticky;
      top: 20px;
    }

    #stage {
      width: 100%;
      height: 68vh;
      min-height: 420px;
      max-height: 700px;
    }

    #controls { display: flex; flex-direction: column; gap: 9px; align-items: center; width: 100%; }
    .move-row { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; }
    .move-label { font-size: 12px; color: #6c7b99; letter-spacing: 1px; margin-bottom: -2px; }

    button {
      border: none;
      border-radius: 9px;
      padding: 10px 15px;
      font-size: 15px;
      font-weight: 600;
      cursor: pointer;
      background: #37425c;
      color: #fff;
      box-shadow: 0 3px 0 #1a2130;
      transition: transform .06s, background .15s, box-shadow .06s;
      min-width: 48px;
      letter-spacing: .5px;
    }
    button:hover { background: #46557a; }
    button:active { transform: translateY(3px); box-shadow: 0 0 0 #1a2130; }
    button.rot { background: #6b4fa0; }
    button.rot:hover { background: #8462c4; }
    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; }

    .hint {
      margin-top: 4px;
      font-size: 12.5px;
      color: #6c7b99;
      text-align: center;
      line-height: 1.8;
    }
    kbd {
      background: #2a3344;
      border: 1px solid #4a5a7a;
      border-radius: 5px;
      padding: 1px 7px;
      font-size: 12px;
    }

    #recordBox { display: flex; flex-direction: column; gap: 10px; }
    #recordTitle { font-size: 15px; font-weight: 700; color: #cfd9ea; letter-spacing: 1px; text-align: center; }
    #recordPanel {
      background: var(--panel);
      border: 1px solid #2a3344;
      border-radius: 10px;
      padding: 14px 16px;
      font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
      font-size: 13px;
      line-height: 1.65;
      color: #cfd9ea;
      white-space: pre-wrap;
      word-break: break-all;
      height: 68vh;
      min-height: 420px;
      max-height: 700px;
      overflow: auto;
      margin: 0;
    }
    #copyBtn { width: 100%; }

    #seqRow { width: 100%; margin-top: 4px; flex-wrap: nowrap; }
    #seqInput {
      flex: 1;
      min-width: 150px;
      padding: 9px 12px;
      border-radius: 8px;
      border: 1px solid #4a5a7a;
      background: #161b26;
      color: #e8edf5;
      font-size: 14px;
      font-family: "Cascadia Code", "Consolas", monospace;
    }
    #seqInput:focus { outline: none; border-color: #6d88c8; }

    #formulaRow { width: 100%; margin-top: 2px; gap: 6px; flex-wrap: nowrap; }
    #formulaNameInput,
    #formulaContentInput {
      padding: 9px 12px;
      border-radius: 8px;
      border: 1px solid #4a5a7a;
      background: #161b26;
      color: #e8edf5;
      font-size: 13px;
      font-family: "Cascadia Code", "Consolas", monospace;
    }
    #formulaNameInput { width: 100px; flex: 0 0 100px; }
    #formulaContentInput { flex: 1; min-width: 120px; }
    #formulaNameInput:focus,
    #formulaContentInput:focus { outline: none; border-color: #6d88c8; }
    #formulaButtons { margin-top: 2px; }
    #formulaButtons button.formula { background: #8a5f9e; }
    #formulaButtons button.formula:hover { background: #a174b8; }

    @media (max-width: 1240px) {
      #layout { flex-wrap: wrap; }
      #leftCol { flex: 0 0 380px; width: 380px; }
      #rightCol { flex: 0 0 300px; width: 300px; }
    }
    @media (max-width: 1000px) {
      #layout { flex-direction: column; align-items: center; }
      #leftCol { flex: 1 1 auto; width: 100%; max-width: 540px; }
      #middleCol { flex: 1 1 auto; width: 100%; max-width: 580px; }
      #rightCol { width: 100%; max-width: 580px; position: static; }
      #stage { height: 52vh; min-height: 320px; max-height: 540px; }
      #recordPanel { height: auto; max-height: 340px; min-height: 0; }
    }
    @media (max-width: 560px) {
      button { padding: 8px 10px; font-size: 13px; min-width: 40px; }
      #formulaNameInput { width: 84px; flex-basis: 84px; }
      #seqRow, #formulaRow { flex-wrap: wrap; }
    }
  </style>
</head>
<body>
  <h1>五阶魔方 · 层操作</h1>
  <div id="status">加载中…</div>

  <div id="layout">
    <div id="leftCol">
      <div id="stage"></div>
    </div>

    <div id="middleCol">
      <div id="controls">
        <div class="move-label">整体转体(不计步数)</div>
        <div class="move-row" id="rotButtons"></div>
        <div class="move-row">
          <button class="tool" id="scrambleBtn">打乱</button>
          <button class="tool warn" id="resetBtn">复原</button>
        </div>
        <div class="move-row" id="seqRow">
          <input id="seqInput" type="text"
                 placeholder="例如: R R2 R3 R4 F' L2 U2' D" />
          <button class="tool" id="seqBtn">执行</button>
        </div>
        <div class="move-row" id="formulaRow">
          <input id="formulaNameInput" type="text" placeholder="公式名称" />
          <input id="formulaContentInput" type="text"
                 placeholder="公式内容,如 R2 U R2' U'" />
          <button class="tool" id="addFormulaBtn">添加公式</button>
        </div>
        <div class="move-row" id="formulaButtons"></div>
        <div class="move-row">
          <button class="tool" id="autoRunBtn">自动运行</button>
        </div>
      </div>
      <div class="hint">
        <kbd>R</kbd> 第一层,<kbd>R2</kbd> 第一+第二层,<kbd>R3</kbd> 第一+第二+第三层,<kbd>R4</kbd> 第一+第二+第三+第四层。<br>
        <kbd>R5</kbd> 等价于 <kbd>x</kbd>(整体转体)。同理 U / F / D / L / B。<br>
        转体:<kbd>X</kbd> 绕 X 轴 (R层),<kbd>Y</kbd> 绕 Z 轴 (F层),<kbd>Z</kbd> 绕 Y 轴 (U层)。<kbd>Shift</kbd> + 字母为逆时针。<br>
        右键公式按钮可删除。自动运行需启动 server.js 并通过 http://localhost:3000 打开。
      </div>
    </div>

    <div id="rightCol">
      <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));
renderer.setSize(stage.clientWidth, stage.clientHeight);
stage.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, stage.clientWidth / stage.clientHeight, 0.1, 100);
camera.position.set(6.2, 5.4, 8.4);
camera.lookAt(0, -0.8, 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 OUT = 2.0;
const IN = 1.0;
const MID = 0.0;
const SIZE = 0.92;

// 基础轴定义
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) }
};

// 构建层定义:
// R: 第一层 (x=2)
// R2: 第一层+第二层 (x=2,1)
// R3: 第一层+第二层+第三层 (x=2,1,0)
// R4: 第一层+第二层+第三层+第四层 (x=2,1,0,-1)
// R5: 所有层 (x=2,1,0,-1,-2) 等价于整体转体 x
function buildLayerDefs() {
  const N = 5;
  const defs = {};
  const faces = ['U','D','R','L','F','B'];
  const posMap = { 1: [2], 2: [2,1], 3: [2,1,0], 4: [2,1,0,-1] };
  
  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;
      const targets = posMap[layer].map(p => p * base.side);
      defs[key] = {
        axis: base.axis,
        side: base.side,
        vec: base.vec,
        targets: targets,
        layer: layer,
      };
    }
  }
  return defs;
}

const LAYER_DEFS = buildLayerDefs();

// 整体转体定义:
// x: 绕 X 轴旋转 (等价于 R 层整体转体)
// y: 绕 Z 轴旋转 (等价于 F 层整体转体) 
// z: 绕 Y 轴旋转 (等价于 U 层整体转体)
const ROT_DEF = {
  x: { axis: 'x', all: true },
  y: { axis: 'z', all: true },
  z: { axis: 'y', all: true }
};

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 });
}

// ---------- 98 个表面块 ----------
let cubies = [];
const pivot = new THREE.Group();
scene.add(pivot);

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 };
}

function buildCube() {
  cubies.forEach(c => scene.remove(c.mesh));
  cubies = [];
  const pos = [-OUT, -IN, MID, IN, OUT];
  for (const x of pos)
    for (const y of pos)
      for (const z of pos) {
        if (Math.abs(x) === OUT || Math.abs(y) === OUT || Math.abs(z) === OUT) {
          cubies.push(createCubie(x, y, z));
        }
      }
}

// ---------- 旋转引擎 ----------
const moveQueue = [];
let busy = false;
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;

function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

function snap(v) { return Math.round(v * 2) / 2; }

// 根据层定义选择要旋转的块
function selectCubiesForLayer(faceKey) {
  const def = LAYER_DEFS[faceKey];
  if (!def) return null;
  const targets = def.targets;
  return cubies.filter(c => {
    const val = c.mesh.position[def.axis];
    return targets.some(t => Math.abs(val - t) < 0.01);
  });
}

function performMove(faceKey, dir, duration, done) {
  // 检查是否为整体转体
  if (ROT_DEF[faceKey]) {
    performRotation(cubies.slice(), ROT_DEF[faceKey].axis, dir, duration, done);
    return;
  }

  const def = LAYER_DEFS[faceKey];
  if (!def) {
    done();
    return;
  }

  const selected = selectCubiesForLayer(faceKey);
  if (!selected || selected.length === 0) {
    done();
    return;
  }

  const flip = def.side > 0 ? 1 : -1;
  performRotation(selected, def.axis, dir * flip, duration, done);
}

function performRotation(selected, axis, dir, duration, done) {
  selected.forEach(c => pivot.attach(c.mesh));
  const angle = -dir * Math.PI / 2;
  const start = performance.now();

  function frame(now) {
    let t = Math.min(1, (now - start) / duration);
    const e = easeInOutCubic(t);
    pivot.rotation.set(0, 0, 0);
    pivot.rotation[axis] = angle * e;

    if (t < 1) {
      requestAnimationFrame(frame);
    } else {
      pivot.rotation.set(0, 0, 0);
      pivot.rotation[axis] = angle;
      selected.forEach(c => {
        scene.attach(c.mesh);
        c.mesh.position.x = snap(c.mesh.position.x);
        c.mesh.position.y = snap(c.mesh.position.y);
        c.mesh.position.z = snap(c.mesh.position.z);
      });
      pivot.rotation.set(0, 0, 0);
      done();
    }
  }
  requestAnimationFrame(frame);
}

function enqueueMove(faceKey, dir, duration, opts = {}) {
  moveQueue.push({
    faceKey, dir, duration,
    count: opts.count !== false,
    record: !!opts.record,
    silent: opts.silent === true,
    onDone: opts.onDone
  });
  if (!busy) processQueue();
}

function processQueue() {
  if (moveQueue.length === 0) { busy = false; return; }
  busy = true;
  const m = moveQueue.shift();

  if (m.record) userSeq.push({ face: m.faceKey, dir: m.dir });

  performMove(m.faceKey, m.dir, m.duration, () => {
    if (m.count) {
      moveCount++;
      if (isSolved()) {
        statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
        statusEl.classList.add('solved');
        statusEl.classList.remove('error');
      } else {
        statusEl.textContent = '步数:' + moveCount;
        statusEl.classList.remove('solved');
        statusEl.classList.remove('error');
      }
    }
    if (m.onDone) m.onDone();
    if (!m.silent) updateRecordPanel();
    processQueue();
  });
}

// ---------- 工具函数 ----------
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 !== 25) return false;
    for (let i = 1; i < 25; i++) {
      if (s[i] !== s[0]) return false;
    }
  }
  return true;
}

// ---------- 局面读取 ----------
function getFaceSlots(face) {
  const s = [];
  for (let r = 0; r < 5; r++)
    for (let c = 0; c < 5; c++) {
      const x = c - 2.0;
      const yTop = 2.0 - r;
      const z = r - 2.0;
      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.mesh.position.distanceTo(p) < 0.01);
    if (!cubie) { out += '?'; continue; }
    const n0 = n.clone().applyQuaternion(cubie.mesh.quaternion.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 ? "'" : '');
}

function seqToText(seq) {
  return seq.length ? seq.map(moveNotation).join(' ') : '(无)';
}

// ---------- 棋谱 ----------
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();
}

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) {
    // 匹配: 字母 + 可选数字(2-4) + 可选 '
    // 例如: R, R2, R3, R4, U', F2, L3'
    let m = raw.match(/^([URFDLB])([2-4])?([''])?$/);
    if (m) {
      const letter = m[1];
      const layerNum = m[2] || '';
      const dir = m[3] === "'" ? -1 : 1;
      let faceKey = letter + layerNum;
      if (layerNum === '') faceKey = letter;
      if (!LAYER_DEFS[faceKey]) {
        errors.push('未知操作: ' + raw);
        continue;
      }
      moves.push({ face: faceKey, dir });
      continue;
    }

    // 整体转体 x, y, z
    const rotMatch = raw.match(/^([xyz])([''])?$/);
    if (rotMatch) {
      const letter = rotMatch[1];
      const dir = rotMatch[2] === "'" ? -1 : 1;
      moves.push({ face: letter, dir });
      continue;
    }

    errors.push('无法解析: ' + raw);
  }

  if (errors.length > 0) {
    return { moves, errors };
  }
  return { moves, errors: [] };
}

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');

  moves.forEach((mv, i) => {
    const isLayer = !!LAYER_DEFS[mv.face] || !!ROT_DEF[mv.face];
    const isLast = i === moves.length - 1;
    enqueueMove(mv.face, mv.dir, 2, {
      count: isLayer && !ROT_DEF[mv.face],
      record: true,
      silent: true,
      onDone: isLast ? () => {
        updateRecordPanel();
        if (!isSolved()) {
          statusEl.textContent = '序列执行完成(' + moves.length + ' 步操作)';
          statusEl.classList.remove('solved');
        }
      } : undefined
    });
  });
}

// ---------- 自动运行 ----------
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));
  }

  executeSequence(content);
}

// ---------- 公式录入 ----------
const STORAGE_KEY = 'cubeFormulas';
let formulas = [];

function loadFormulas() {
  try {
    formulas = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    if (!Array.isArray(formulas)) formulas = [];
  } catch (e) {
    formulas = [];
  }
}

function saveFormulas() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(formulas));
}

function renderFormulaButtons() {
  const box = document.getElementById('formulaButtons');
  box.innerHTML = '';

  formulas.forEach((f, idx) => {
    const btn = document.createElement('button');
    btn.className = 'formula';
    btn.textContent = f.name;
    btn.title = f.content + '(右键删除)';
    btn.addEventListener('click', () => executeSequence(f.content));

    btn.addEventListener('contextmenu', e => {
      e.preventDefault();
      if (confirm('删除公式「' + f.name + '」?')) {
        formulas.splice(idx, 1);
        saveFormulas();
        renderFormulaButtons();
      }
    });

    box.appendChild(btn);
  });
}

function addFormula() {
  const nameInput = document.getElementById('formulaNameInput');
  const contentInput = document.getElementById('formulaContentInput');

  const name = nameInput.value.trim();
  const content = contentInput.value.trim();

  if (!name) {
    statusEl.textContent = '请填写公式名称';
    statusEl.classList.remove('solved', 'error');
    return;
  }
  const { errors } = parseSequence(content);
  if (errors.length > 0) {
    statusEl.textContent = '公式内容有误: ' + errors.join('; ');
    statusEl.classList.add('error');
    statusEl.classList.remove('solved');
    return;
  }

  const exist = formulas.findIndex(f => f.name === name);
  if (exist >= 0) {
    formulas[exist].content = content;
  } else {
    formulas.push({ name, content });
  }

  saveFormulas();
  renderFormulaButtons();
  nameInput.value = '';
  contentInput.value = '';
  statusEl.textContent = '已添加公式:' + name;
  statusEl.classList.remove('solved', 'error');
}

// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
  moveQueue.length = 0;
  busy = false;
  cubies.forEach(c => { if (c.mesh.parent === pivot) scene.attach(c.mesh); });
  pivot.rotation.set(0, 0, 0);
  cubies.forEach(c => {
    c.mesh.position.copy(c.home);
    c.mesh.quaternion.identity();
  });
}

function generateScramble(n = 40) {
  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;
}

function scramble() {
  resetCubeInternal();
  userSeq = [];
  initialState = null;
  moveCount = 0;
  statusEl.textContent = '打乱中…';
  statusEl.classList.remove('solved', 'error');

  scrambleSeq = generateScramble(40);
  if (scrambleSeq.length === 0) {
    statusEl.textContent = '打乱序列为空';
    return;
  }
  scrambleSeq.forEach((m, idx) => {
    enqueueMove(m.face, m.dir, 70, {
      count: false,
      record: false,
      onDone: idx === scrambleSeq.length - 1 ? () => {
        initialState = getState();
        moveCount = 0;
        statusEl.textContent = '已打乱,开始复原吧';
        statusEl.classList.remove('error');
        updateRecordPanel();
      } : undefined
    });
  });
}

function resetCube() {
  resetCubeInternal();
  moveCount = 0;
  scrambleSeq = [];
  userSeq = [];
  initialState = null;
  statusEl.textContent = '已还原到初始状态';
  statusEl.classList.remove('solved', 'error');
  updateRecordPanel();
}

// ---------- 按钮 & 键盘 ----------
function buildButtons() {
  const rotBox = document.getElementById('rotButtons');
  for (const face of ['x','y','z']) {
    const cw = document.createElement('button');
    cw.className = 'rot';
    cw.textContent = face;
    cw.title = face + ' 整体顺时针';
    cw.addEventListener('click', () => enqueueMove(face, 1, 220, { count: false, record: true }));

    const ccw = document.createElement('button');
    ccw.className = 'rot';
    ccw.textContent = face + "'";
    ccw.title = face + ' 整体逆时针';
    ccw.addEventListener('click', () => enqueueMove(face, -1, 220, { count: false, record: true }));

    rotBox.appendChild(cw);
    rotBox.appendChild(ccw);
  }

  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('addFormulaBtn').addEventListener('click', addFormula);

  const formulaContentInput = document.getElementById('formulaContentInput');
  formulaContentInput.addEventListener('keydown', e => {
    if (e.key === 'Enter') {
      e.preventDefault();
      addFormula();
    }
  });

  document.getElementById('autoRunBtn').addEventListener('click', autoRun);

  loadFormulas();
  renderFormulaButtons();

  // 键盘快捷键
  window.addEventListener('keydown', e => {
    const key = e.key.toUpperCase();
    // U/R/F/D/L/B 默认第一层
    if (/^[URFDLB]$/.test(key)) {
      const faceKey = key;
      if (LAYER_DEFS[faceKey]) {
        const dir = e.shiftKey ? -1 : 1;
        enqueueMove(faceKey, dir, 220, { count: true, record: true });
      }
      return;
    }
    // x, y, z 整体转体
    if (/^[XYZ]$/.test(key)) {
      const face = key.toLowerCase();
      const dir = e.shiftKey ? -1 : 1;
      enqueueMove(face, dir, 220, { count: false, record: true });
      return;
    }
  });
}

// ---------- 渲染循环 ----------
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

window.addEventListener('resize', () => {
  const w = stage.clientWidth, h = stage.clientHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
});

// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
updateRecordPanel();
setTimeout(() => {
  scramble();
}, 100);
animate();
</script>
</body>
</html>

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值