+
@@ -765,6 +872,7 @@ onUnmounted(() => {
{
@click.self="closeAddModal"
>
-
+
-
@@ -796,10 +907,10 @@ onUnmounted(() => {
- {{ rotationDebugTarget !== null ? rotationDebugTarget : '-' }}
+ {{ rotationDebugTarget !== null ? rotationDebugTarget : "-" }}
- {{ rotationDebugCurrent !== null ? rotationDebugCurrent : '-' }}
+ {{ rotationDebugCurrent !== null ? rotationDebugCurrent : "-" }}
@@ -923,8 +1034,10 @@ onUnmounted(() => {
/* Projection Styles */
.projections {
position: absolute;
- top: 0; left: 0;
- width: 0; height: 0;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 0;
pointer-events: none; /* Let clicks pass through to the cube */
transform-style: preserve-3d;
}
@@ -969,7 +1082,6 @@ onUnmounted(() => {
transform: translateX(0) translateY(350px) translateZ(0) rotateX(90deg);
}
-
.sticker {
position: absolute;
width: 100px;
@@ -986,35 +1098,59 @@ onUnmounted(() => {
/* Pseudo-element for the colored sticker part */
.sticker::after {
- content: '';
+ content: "";
position: absolute;
top: 4px;
left: 4px;
right: 4px;
bottom: 4px;
border-radius: 8px; /* Rounded sticker */
- box-shadow: inset 0 0 5px rgba(0,0,0,0.3); /* Inner depth */
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3); /* Inner depth */
z-index: 1;
}
/* Sticker Positions relative to Cubie Center */
-.sticker.up { transform: rotateX(90deg) translateZ(50px); }
-.sticker.down { transform: rotateX(-90deg) translateZ(50px); }
-.sticker.front { transform: translateZ(50px); }
-.sticker.back { transform: rotateY(180deg) translateZ(50px); }
-.sticker.left { transform: rotateY(-90deg) translateZ(50px); }
-.sticker.right { transform: rotateY(90deg) translateZ(50px); }
+.sticker.up {
+ transform: rotateX(90deg) translateZ(50px);
+}
+.sticker.down {
+ transform: rotateX(-90deg) translateZ(50px);
+}
+.sticker.front {
+ transform: translateZ(50px);
+}
+.sticker.back {
+ transform: rotateY(180deg) translateZ(50px);
+}
+.sticker.left {
+ transform: rotateY(-90deg) translateZ(50px);
+}
+.sticker.right {
+ transform: rotateY(90deg) translateZ(50px);
+}
/* Colors - apply to the pseudo-element */
-.white::after { background: #E0E0E0; }
-.yellow::after { background: #FFD500; }
-.green::after { background: #009E60; }
-.blue::after { background: #0051BA; }
-.orange::after { background: #FF5800; }
-.red::after { background: #C41E3A; }
+.white::after {
+ background: #e0e0e0;
+}
+.yellow::after {
+ background: #ffd500;
+}
+.green::after {
+ background: #009e60;
+}
+.blue::after {
+ background: #0051ba;
+}
+.orange::after {
+ background: #ff5800;
+}
+.red::after {
+ background: #c41e3a;
+}
/* Black internal faces - no sticker needed */
-.black {
+.black {
background: #050505;
border: 1px solid #000;
border-radius: 0;
@@ -1024,5 +1160,4 @@ onUnmounted(() => {
.black::after {
display: none;
}
-
diff --git a/src/composables/useCube.js b/src/composables/useCube.js
index b87f06c..818968e 100644
--- a/src/composables/useCube.js
+++ b/src/composables/useCube.js
@@ -1,9 +1,11 @@
-
-import { ref, computed } from 'vue';
-import { COLORS, FACES } from '../utils/CubeModel';
+import { ref, computed } from "vue";
+import { COLORS, FACES } from "../utils/CubeModel";
// Singleton worker
-const worker = new Worker(new URL('../workers/Cube.worker.js', import.meta.url), { type: 'module' });
+const worker = new Worker(
+ new URL("../workers/Cube.worker.js", import.meta.url),
+ { type: "module" },
+);
// Reactive state
const cubies = ref([]);
@@ -12,35 +14,37 @@ const validationResult = ref(null);
worker.onmessage = (e) => {
const { type, payload } = e.data;
- if (type === 'STATE_UPDATE') {
+ if (type === "STATE_UPDATE") {
cubies.value = payload.cubies;
isReady.value = true;
- } else if (type === 'VALIDATION_RESULT') {
+ } else if (type === "VALIDATION_RESULT") {
validationResult.value = payload;
- } else if (type === 'ERROR') {
- console.error('Worker Error:', payload);
+ } else if (type === "ERROR") {
+ console.error("Worker Error:", payload);
}
};
// Init worker
-worker.postMessage({ type: 'INIT' });
+worker.postMessage({ type: "INIT" });
export function useCube() {
-
const initCube = () => {
- worker.postMessage({ type: 'RESET' });
+ worker.postMessage({ type: "RESET" });
};
- const rotateLayer = (axis, index, direction) => {
- worker.postMessage({ type: 'ROTATE_LAYER', payload: { axis, index, direction } });
+ const rotateLayer = (axis, index, direction, steps = 1) => {
+ worker.postMessage({
+ type: "ROTATE_LAYER",
+ payload: { axis, index, direction, steps },
+ });
};
const turn = (move) => {
- worker.postMessage({ type: 'TURN', payload: { move } });
+ worker.postMessage({ type: "TURN", payload: { move } });
};
const validate = () => {
- worker.postMessage({ type: 'VALIDATE' });
+ worker.postMessage({ type: "VALIDATE" });
};
return {
@@ -52,6 +56,6 @@ export function useCube() {
turn,
validate,
COLORS,
- FACES
+ FACES,
};
}
diff --git a/src/composables/useSettings.js b/src/composables/useSettings.js
index cc8f7eb..e68e2cc 100644
--- a/src/composables/useSettings.js
+++ b/src/composables/useSettings.js
@@ -1,10 +1,10 @@
-import { ref } from 'vue';
+import { ref } from "vue";
let initialCubeTranslucent = false;
try {
- const stored = localStorage.getItem('cubeTranslucent');
+ const stored = localStorage.getItem("cubeTranslucent");
if (stored !== null) {
- initialCubeTranslucent = stored === 'true';
+ initialCubeTranslucent = stored === "true";
}
} catch (e) {}
@@ -14,12 +14,12 @@ export function useSettings() {
const toggleCubeTranslucent = () => {
isCubeTranslucent.value = !isCubeTranslucent.value;
try {
- localStorage.setItem('cubeTranslucent', String(isCubeTranslucent.value));
+ localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value));
} catch (e) {}
};
return {
isCubeTranslucent,
- toggleCubeTranslucent
+ toggleCubeTranslucent,
};
}
diff --git a/src/config/animationSettings.js b/src/config/animationSettings.js
index 42b2585..d496ed0 100644
--- a/src/config/animationSettings.js
+++ b/src/config/animationSettings.js
@@ -1 +1 @@
-export const LAYER_ANIMATION_DURATION = 200
+export const LAYER_ANIMATION_DURATION = 200;
diff --git a/src/main.js b/src/main.js
index 2425c0f..3c9bfeb 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,5 +1,5 @@
-import { createApp } from 'vue'
-import './style.css'
-import App from './App.vue'
+import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
-createApp(App).mount('#app')
+createApp(App).mount("#app");
diff --git a/src/utils/CubeLogicAdapter.js b/src/utils/CubeLogicAdapter.js
new file mode 100644
index 0000000..fc807ac
--- /dev/null
+++ b/src/utils/CubeLogicAdapter.js
@@ -0,0 +1,59 @@
+import { DeepCube, MOVES } from './DeepCube.js';
+import { CubeModel } from './CubeModel.js';
+
+export class RubiksJSModel {
+ constructor() {
+ this.state = new DeepCube();
+ this.visual = new CubeModel();
+ }
+
+ reset() {
+ this.state = new DeepCube();
+ this.visual = new CubeModel();
+ }
+
+ rotateLayer(axis, index, dir, steps = 1) {
+ let move = '';
+ if (axis === 'y') {
+ if (index === 1) move = dir === 1 ? "U'" : "U";
+ else if (index === -1) move = dir === -1 ? "D'" : "D";
+ } else if (axis === 'x') {
+ if (index === 1) move = dir === 1 ? "R'" : "R";
+ else if (index === -1) move = dir === -1 ? "L'" : "L";
+ } else if (axis === 'z') {
+ if (index === 1) move = dir === 1 ? "F'" : "F";
+ else if (index === -1) move = dir === -1 ? "B'" : "B";
+ }
+
+ if (move) {
+ for (let i = 0; i < steps; i++) {
+ try {
+ this.state = this.state.multiply(MOVES[move]);
+ } catch (e) {
+ console.error('[RubiksJSModel] Failed to apply move:', move, e);
+ }
+ this.visual.rotateLayer(axis, index, dir);
+ }
+ }
+ }
+
+ applyTurn(move) {
+ if (!move) return;
+ try {
+ this.state = this.state.multiply(MOVES[move]);
+ } catch (e) {
+ console.error('[RubiksJSModel] Failed to apply direct move:', move, e);
+ }
+ this.visual.applyMove(move);
+ }
+
+ toCubies() {
+ return this.visual.toCubies();
+ }
+
+ validate() {
+ const valid = this.state.isValid();
+ return { valid, errors: valid ? [] : ['Invalid cube configuration (Parity or Orientation rules violated)'] };
+ }
+}
+
diff --git a/src/utils/CubeModel.js b/src/utils/CubeModel.js
index 2f35be1..6c8d521 100644
--- a/src/utils/CubeModel.js
+++ b/src/utils/CubeModel.js
@@ -12,22 +12,22 @@
*/
export const COLORS = {
- WHITE: 'white',
- YELLOW: 'yellow',
- ORANGE: 'orange',
- RED: 'red',
- GREEN: 'green',
- BLUE: 'blue',
- BLACK: 'black'
+ WHITE: "white",
+ YELLOW: "yellow",
+ ORANGE: "orange",
+ RED: "red",
+ GREEN: "green",
+ BLUE: "blue",
+ BLACK: "black",
};
export const FACES = {
- UP: 'up',
- DOWN: 'down',
- LEFT: 'left',
- RIGHT: 'right',
- FRONT: 'front',
- BACK: 'back',
+ UP: "up",
+ DOWN: "down",
+ LEFT: "left",
+ RIGHT: "right",
+ FRONT: "front",
+ BACK: "back",
};
// Standard Face Colors (Solved State)
@@ -99,7 +99,7 @@ export class CubeModel {
*/
rotateLayer(axis, index, direction) {
// Determine the relevant cubies in the slice
- const slice = this.cubies.filter(c => c[axis] === index);
+ const slice = this.cubies.filter((c) => c[axis] === index);
// Coordinate rotation (Matrix Logic)
// 90 deg CW rotation formulas:
@@ -120,7 +120,7 @@ export class CubeModel {
// If direction is -1: Inverse.
- slice.forEach(cubie => {
+ slice.forEach((cubie) => {
this._rotateCubieCoordinates(cubie, axis, direction);
this._rotateCubieFaces(cubie, axis, direction);
});
@@ -129,7 +129,7 @@ export class CubeModel {
_rotateCubieCoordinates(cubie, axis, direction) {
const { x, y, z } = cubie;
- if (axis === 'x') {
+ if (axis === "x") {
if (direction === 1) {
cubie.y = -z;
cubie.z = y;
@@ -137,7 +137,7 @@ export class CubeModel {
cubie.y = z;
cubie.z = -y;
}
- } else if (axis === 'y') {
+ } else if (axis === "y") {
if (direction === 1) {
cubie.z = -x;
cubie.x = z;
@@ -145,11 +145,13 @@ export class CubeModel {
cubie.z = x;
cubie.x = -z;
}
- } else if (axis === 'z') {
- if (direction === 1) { // CW
+ } else if (axis === "z") {
+ if (direction === 1) {
+ // CW
cubie.x = -y;
cubie.y = x;
- } else { // CCW
+ } else {
+ // CCW
cubie.x = y;
cubie.y = -x;
}
@@ -165,38 +167,44 @@ export class CubeModel {
const f = { ...cubie.faces };
- if (axis === 'x') {
- if (direction === 1) { // Up -> Front -> Down -> Back -> Up
+ if (axis === "x") {
+ if (direction === 1) {
+ // Up -> Front -> Down -> Back -> Up
cubie.faces[FACES.FRONT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.BACK];
// Left/Right unchanged in position, but might rotate? No, faces are solid colors.
- } else { // Up -> Back -> Down -> Front -> Up
+ } else {
+ // Up -> Back -> Down -> Front -> Up
cubie.faces[FACES.BACK] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.FRONT];
}
- } else if (axis === 'y') {
- if (direction === 1) { // Front -> Right -> Back -> Left -> Front
+ } else if (axis === "y") {
+ if (direction === 1) {
+ // Front -> Right -> Back -> Left -> Front
cubie.faces[FACES.RIGHT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.LEFT];
- } else { // Front -> Left -> Back -> Right -> Front
+ } else {
+ // Front -> Left -> Back -> Right -> Front
cubie.faces[FACES.LEFT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.RIGHT];
}
- } else if (axis === 'z') {
- if (direction === 1) { // CCW: Up -> Left -> Down -> Right -> Up
+ } else if (axis === "z") {
+ if (direction === 1) {
+ // CCW: Up -> Left -> Down -> Right -> Up
cubie.faces[FACES.LEFT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.RIGHT];
- } else { // CW: Up -> Right -> Down -> Left -> Up
+ } else {
+ // CW: Up -> Right -> Down -> Left -> Up
cubie.faces[FACES.RIGHT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
@@ -208,12 +216,12 @@ export class CubeModel {
toCubies() {
// Return copy of state for rendering
// CubeCSS expects array of objects with x, y, z, faces
- return this.cubies.map(c => ({
+ return this.cubies.map((c) => ({
id: c.id,
x: c.x,
y: c.y,
z: c.z,
- faces: { ...c.faces }
+ faces: { ...c.faces },
}));
}
@@ -238,30 +246,54 @@ export class CubeModel {
// B (CW around -Z): 1 (since Z(1) is CW around -Z)
switch (base) {
- case 'U': direction = 1; break;
- case 'D': direction = -1; break;
- case 'L': direction = -1; break;
- case 'R': direction = 1; break;
- case 'F': direction = -1; break;
- case 'B': direction = 1; break;
+ case "U":
+ direction = 1;
+ break;
+ case "D":
+ direction = -1;
+ break;
+ case "L":
+ direction = -1;
+ break;
+ case "R":
+ direction = 1;
+ break;
+ case "F":
+ direction = -1;
+ break;
+ case "B":
+ direction = 1;
+ break;
}
if (modifier === "'") direction *= -1;
- if (modifier === '2') {
+ if (modifier === "2") {
// 2 moves. Direction doesn't matter for 180, but let's keep it.
// We will call rotateLayer twice.
}
- const count = modifier === '2' ? 2 : 1;
+ const count = modifier === "2" ? 2 : 1;
for (let i = 0; i < count; i++) {
switch (base) {
- case 'U': this.rotateLayer('y', 1, direction); break;
- case 'D': this.rotateLayer('y', -1, direction); break;
- case 'L': this.rotateLayer('x', -1, direction); break;
- case 'R': this.rotateLayer('x', 1, direction); break;
- case 'F': this.rotateLayer('z', 1, direction); break;
- case 'B': this.rotateLayer('z', -1, direction); break;
+ case "U":
+ this.rotateLayer("y", 1, direction);
+ break;
+ case "D":
+ this.rotateLayer("y", -1, direction);
+ break;
+ case "L":
+ this.rotateLayer("x", -1, direction);
+ break;
+ case "R":
+ this.rotateLayer("x", 1, direction);
+ break;
+ case "F":
+ this.rotateLayer("z", 1, direction);
+ break;
+ case "B":
+ this.rotateLayer("z", -1, direction);
+ break;
}
}
}
@@ -282,25 +314,46 @@ export class CubeModel {
for (let c = 0; c < 3; c++) {
let cubie;
// Map r,c to x,y,z based on face
- if (face === FACES.UP) { // y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
+ if (face === FACES.UP) {
+ // y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
// Standard U face view: Top Left is Back Left (-1, 1, -1).
// Row 0 (Top of U face) is Back.
// Row 2 (Bottom of U face) is Front.
- cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1)); // Wait.
+ cubie = this.cubies.find(
+ (cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1,
+ ); // Wait.
// Back is z=-1. Front is z=1.
// Visual Top of U face is Back (z=-1).
// Visual Bottom of U face is Front (z=1).
- cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1 - 2 * r)); // Complicated.
+ cubie = this.cubies.find(
+ (cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1 - 2 * r,
+ ); // Complicated.
// Let's just find by strict coordinates
// r=0 -> z=-1. r=1 -> z=0. r=2 -> z=1.
// c=0 -> x=-1. c=1 -> x=0. c=2 -> x=1.
- cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1));
- }
- else if (face === FACES.DOWN) cubie = this.cubies.find(cu => cu.y === -1 && cu.x === (c - 1) && cu.z === (1 - r)); // Down View?
- else if (face === FACES.FRONT) cubie = this.cubies.find(cu => cu.z === 1 && cu.x === (c - 1) && cu.y === (1 - r));
- else if (face === FACES.BACK) cubie = this.cubies.find(cu => cu.z === -1 && cu.x === (1 - c) && cu.y === (1 - r));
- else if (face === FACES.LEFT) cubie = this.cubies.find(cu => cu.x === -1 && cu.z === (1 - c) && cu.y === (1 - r)); // Left view z order?
- else if (face === FACES.RIGHT) cubie = this.cubies.find(cu => cu.x === 1 && cu.z === (c - 1) && cu.y === (1 - r));
+ cubie = this.cubies.find(
+ (cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1,
+ );
+ } else if (face === FACES.DOWN)
+ cubie = this.cubies.find(
+ (cu) => cu.y === -1 && cu.x === c - 1 && cu.z === 1 - r,
+ ); // Down View?
+ else if (face === FACES.FRONT)
+ cubie = this.cubies.find(
+ (cu) => cu.z === 1 && cu.x === c - 1 && cu.y === 1 - r,
+ );
+ else if (face === FACES.BACK)
+ cubie = this.cubies.find(
+ (cu) => cu.z === -1 && cu.x === 1 - c && cu.y === 1 - r,
+ );
+ else if (face === FACES.LEFT)
+ cubie = this.cubies.find(
+ (cu) => cu.x === -1 && cu.z === 1 - c && cu.y === 1 - r,
+ ); // Left view z order?
+ else if (face === FACES.RIGHT)
+ cubie = this.cubies.find(
+ (cu) => cu.x === 1 && cu.z === c - 1 && cu.y === 1 - r,
+ );
if (cubie) {
rowStr += cubie.faces[face][0].toUpperCase() + " ";
@@ -313,16 +366,16 @@ export class CubeModel {
out += "\n";
};
- printFace(FACES.UP, 'U');
- printFace(FACES.DOWN, 'D');
- printFace(FACES.FRONT, 'F');
- printFace(FACES.BACK, 'B');
- printFace(FACES.LEFT, 'L');
- printFace(FACES.RIGHT, 'R');
+ printFace(FACES.UP, "U");
+ printFace(FACES.DOWN, "D");
+ printFace(FACES.FRONT, "F");
+ printFace(FACES.BACK, "B");
+ printFace(FACES.LEFT, "L");
+ printFace(FACES.RIGHT, "R");
return out;
}
scramble(n = 20) {
- const axes = ['x', 'y', 'z'];
+ const axes = ["x", "y", "z"];
const indices = [-1, 1]; // Usually rotate outer layers for scramble
// Actually, scrambling usually involves random face moves (U, D, L, R, F, B)
// U: y=1, dir -1 (Standard CW)
diff --git a/src/utils/DeepCube.js b/src/utils/DeepCube.js
new file mode 100644
index 0000000..3d00f5f
--- /dev/null
+++ b/src/utils/DeepCube.js
@@ -0,0 +1,398 @@
+// Corner indices
+export const CORNERS = {
+ URF: 0,
+ UFL: 1,
+ ULB: 2,
+ UBR: 3,
+ DFR: 4,
+ DLF: 5,
+ DBL: 6,
+ DRB: 7,
+};
+
+// Edge indices
+export const EDGES = {
+ UR: 0,
+ UF: 1,
+ UL: 2,
+ UB: 3,
+ DR: 4,
+ DF: 5,
+ DL: 6,
+ DB: 7,
+ FR: 8,
+ FL: 9,
+ BL: 10,
+ BR: 11,
+};
+
+export class DeepCube {
+ constructor(cp, co, ep, eo) {
+ if (cp && co && ep && eo) {
+ this.cp = [...cp];
+ this.co = [...co];
+ this.ep = [...ep];
+ this.eo = [...eo];
+ } else {
+ // Solved identity state
+ this.cp = [0, 1, 2, 3, 4, 5, 6, 7];
+ this.co = [0, 0, 0, 0, 0, 0, 0, 0];
+ this.ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+ this.eo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+ }
+
+ // Multiply (apply) another cube state to this one.
+ multiply(b) {
+ const cp = new Array(8);
+ const co = new Array(8);
+ const ep = new Array(12);
+ const eo = new Array(12);
+
+ // Corners
+ for (let i = 0; i < 8; i++) {
+ cp[i] = this.cp[b.cp[i]];
+ co[i] = (this.co[b.cp[i]] + b.co[i]) % 3;
+ }
+
+ // Edges
+ for (let i = 0; i < 12; i++) {
+ ep[i] = this.ep[b.ep[i]];
+ eo[i] = (this.eo[b.ep[i]] + b.eo[i]) % 2;
+ }
+
+ return new DeepCube(cp, co, ep, eo);
+ }
+
+ clone() {
+ return new DeepCube(this.cp, this.co, this.ep, this.eo);
+ }
+
+ // Checks if the mathematical state is solvable/possible
+ isValid() {
+ // 1. Edge parity must equal corner parity
+ let edgeParity = 0;
+ for (let i = 11; i >= 0; i--) {
+ for (let j = i - 1; j >= 0; j--) {
+ if (this.ep[j] > this.ep[i]) edgeParity++;
+ }
+ }
+
+ let cornerParity = 0;
+ for (let i = 7; i >= 0; i--) {
+ for (let j = i - 1; j >= 0; j--) {
+ if (this.cp[j] > this.cp[i]) cornerParity++;
+ }
+ }
+ if (edgeParity % 2 !== cornerParity % 2) return false;
+
+ // 2. Edge orientations must sum to even
+ let eoSum = this.eo.reduce((a, b) => a + b, 0);
+ if (eoSum % 2 !== 0) return false;
+
+ // 3. Corner orientations must be divisible by 3
+ let coSum = this.co.reduce((a, b) => a + b, 0);
+ if (coSum % 3 !== 0) return false;
+
+ return true;
+ }
+
+ static fromCubies(cubies) {
+ const c2f = {
+ white: "U",
+ yellow: "D",
+ orange: "L",
+ red: "R",
+ green: "F",
+ blue: "B",
+ };
+
+ const getCubie = (x, y, z) =>
+ cubies.find((c) => c.x === x && c.y === y && c.z === z);
+
+ const baseC = [
+ ["U", "R", "F"],
+ ["U", "F", "L"],
+ ["U", "L", "B"],
+ ["U", "B", "R"],
+ ["D", "F", "R"],
+ ["D", "L", "F"],
+ ["D", "B", "L"],
+ ["D", "R", "B"],
+ ];
+
+ const slotC = [
+ { x: 1, y: 1, z: 1, faces: ["up", "right", "front"] }, // 0: URF
+ { x: -1, y: 1, z: 1, faces: ["up", "front", "left"] }, // 1: UFL
+ { x: -1, y: 1, z: -1, faces: ["up", "left", "back"] }, // 2: ULB
+ { x: 1, y: 1, z: -1, faces: ["up", "back", "right"] }, // 3: UBR
+ { x: 1, y: -1, z: 1, faces: ["down", "front", "right"] }, // 4: DFR
+ { x: -1, y: -1, z: 1, faces: ["down", "left", "front"] }, // 5: DLF
+ { x: -1, y: -1, z: -1, faces: ["down", "back", "left"] }, // 6: DBL
+ { x: 1, y: -1, z: -1, faces: ["down", "right", "back"] }, // 7: DRB
+ ];
+
+ let cp = [],
+ co = [];
+ for (let i = 0; i < 8; i++) {
+ let slot = slotC[i];
+ let c = getCubie(slot.x, slot.y, slot.z);
+ let colors = [
+ c2f[c.faces[slot.faces[0]]],
+ c2f[c.faces[slot.faces[1]]],
+ c2f[c.faces[slot.faces[2]]],
+ ];
+ let perm = baseC.findIndex(
+ (bc) =>
+ colors.includes(bc[0]) &&
+ colors.includes(bc[1]) &&
+ colors.includes(bc[2]),
+ );
+ cp[i] = perm;
+ co[i] = colors.indexOf(baseC[perm][0]);
+ }
+
+ const baseE = [
+ ["U", "R"],
+ ["U", "F"],
+ ["U", "L"],
+ ["U", "B"],
+ ["D", "R"],
+ ["D", "F"],
+ ["D", "L"],
+ ["D", "B"],
+ ["F", "R"],
+ ["F", "L"],
+ ["B", "L"],
+ ["B", "R"],
+ ];
+
+ const slotE = [
+ { x: 1, y: 1, z: 0, faces: ["up", "right"] },
+ { x: 0, y: 1, z: 1, faces: ["up", "front"] },
+ { x: -1, y: 1, z: 0, faces: ["up", "left"] },
+ { x: 0, y: 1, z: -1, faces: ["up", "back"] },
+ { x: 1, y: -1, z: 0, faces: ["down", "right"] },
+ { x: 0, y: -1, z: 1, faces: ["down", "front"] },
+ { x: -1, y: -1, z: 0, faces: ["down", "left"] },
+ { x: 0, y: -1, z: -1, faces: ["down", "back"] },
+ { x: 1, y: 0, z: 1, faces: ["front", "right"] },
+ { x: -1, y: 0, z: 1, faces: ["front", "left"] },
+ { x: -1, y: 0, z: -1, faces: ["back", "left"] },
+ { x: 1, y: 0, z: -1, faces: ["back", "right"] },
+ ];
+
+ let ep = [],
+ eo = [];
+ for (let i = 0; i < 12; i++) {
+ let slot = slotE[i];
+ let c = getCubie(slot.x, slot.y, slot.z);
+ let colors = [c2f[c.faces[slot.faces[0]]], c2f[c.faces[slot.faces[1]]]];
+ let perm = baseE.findIndex(
+ (be) => colors.includes(be[0]) && colors.includes(be[1]),
+ );
+ ep[i] = perm;
+ eo[i] = colors.indexOf(baseE[perm][0]);
+ }
+
+ return new DeepCube(cp, co, ep, eo);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// BASE MOVES DEFINITIONS
+// Represents the effect of 90-degree clockwise faces on the solved state.
+// ----------------------------------------------------------------------------
+
+export const MOVES = {};
+
+// U (Up Face Clockwise)
+MOVES["U"] = new DeepCube(
+ [
+ CORNERS.UBR,
+ CORNERS.URF,
+ CORNERS.UFL,
+ CORNERS.ULB,
+ CORNERS.DFR,
+ CORNERS.DLF,
+ CORNERS.DBL,
+ CORNERS.DRB,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [
+ EDGES.UB,
+ EDGES.UR,
+ EDGES.UF,
+ EDGES.UL,
+ EDGES.DR,
+ EDGES.DF,
+ EDGES.DL,
+ EDGES.DB,
+ EDGES.FR,
+ EDGES.FL,
+ EDGES.BL,
+ EDGES.BR,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+);
+
+// R (Right Face Clockwise)
+MOVES["R"] = new DeepCube(
+ [
+ CORNERS.DFR,
+ CORNERS.UFL,
+ CORNERS.ULB,
+ CORNERS.URF,
+ CORNERS.DRB,
+ CORNERS.DLF,
+ CORNERS.DBL,
+ CORNERS.UBR,
+ ],
+ [2, 0, 0, 1, 1, 0, 0, 2],
+ [
+ EDGES.FR,
+ EDGES.UF,
+ EDGES.UL,
+ EDGES.UB,
+ EDGES.BR,
+ EDGES.DF,
+ EDGES.DL,
+ EDGES.DB,
+ EDGES.DR,
+ EDGES.FL,
+ EDGES.BL,
+ EDGES.UR,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+);
+
+// F (Front Face Clockwise)
+MOVES["F"] = new DeepCube(
+ [
+ CORNERS.UFL,
+ CORNERS.DLF,
+ CORNERS.ULB,
+ CORNERS.UBR,
+ CORNERS.URF,
+ CORNERS.DFR,
+ CORNERS.DBL,
+ CORNERS.DRB,
+ ],
+ [1, 2, 0, 0, 2, 1, 0, 0],
+ [
+ EDGES.UR,
+ EDGES.FL,
+ EDGES.UL,
+ EDGES.UB,
+ EDGES.DR,
+ EDGES.FR,
+ EDGES.DL,
+ EDGES.DB,
+ EDGES.UF,
+ EDGES.DF,
+ EDGES.BL,
+ EDGES.BR,
+ ],
+ [0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0],
+);
+
+// D (Down Face Clockwise)
+MOVES["D"] = new DeepCube(
+ [
+ CORNERS.URF,
+ CORNERS.UFL,
+ CORNERS.ULB,
+ CORNERS.UBR,
+ CORNERS.DLF,
+ CORNERS.DBL,
+ CORNERS.DRB,
+ CORNERS.DFR,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [
+ EDGES.UR,
+ EDGES.UF,
+ EDGES.UL,
+ EDGES.UB,
+ EDGES.DF,
+ EDGES.DL,
+ EDGES.DB,
+ EDGES.DR,
+ EDGES.FR,
+ EDGES.FL,
+ EDGES.BL,
+ EDGES.BR,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+);
+
+// L (Left Face Clockwise)
+MOVES["L"] = new DeepCube(
+ [
+ CORNERS.URF,
+ CORNERS.ULB,
+ CORNERS.DBL,
+ CORNERS.UBR,
+ CORNERS.DFR,
+ CORNERS.UFL,
+ CORNERS.DLF,
+ CORNERS.DRB,
+ ],
+ [0, 1, 2, 0, 0, 2, 1, 0],
+ [
+ EDGES.UR,
+ EDGES.UF,
+ EDGES.BL,
+ EDGES.UB,
+ EDGES.DR,
+ EDGES.DF,
+ EDGES.FL,
+ EDGES.DB,
+ EDGES.FR,
+ EDGES.UL,
+ EDGES.DL,
+ EDGES.BR,
+ ],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+);
+
+// B (Back Face Clockwise)
+MOVES["B"] = new DeepCube(
+ [
+ CORNERS.URF,
+ CORNERS.UFL,
+ CORNERS.UBR,
+ CORNERS.DRB,
+ CORNERS.DFR,
+ CORNERS.DLF,
+ CORNERS.ULB,
+ CORNERS.DBL,
+ ],
+ [0, 0, 1, 2, 0, 0, 2, 1],
+ [
+ EDGES.UR,
+ EDGES.UF,
+ EDGES.UL,
+ EDGES.BR,
+ EDGES.DR,
+ EDGES.DF,
+ EDGES.DL,
+ EDGES.BL,
+ EDGES.FR,
+ EDGES.FL,
+ EDGES.UB,
+ EDGES.DB,
+ ],
+ [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1],
+);
+
+// Generate inverses and 180s
+const faces = ["U", "R", "F", "D", "L", "B"];
+faces.forEach((f) => {
+ const m1 = MOVES[f];
+ const m2 = m1.multiply(m1);
+ const m3 = m2.multiply(m1);
+
+ MOVES[f + "2"] = m2;
+ MOVES[f + "'"] = m3;
+});
diff --git a/src/utils/RubiksJSModel.js b/src/utils/RubiksJSModel.js
deleted file mode 100644
index 54ec11f..0000000
--- a/src/utils/RubiksJSModel.js
+++ /dev/null
@@ -1,172 +0,0 @@
-import { State } from 'rubiks-js/src/state/index.js';
-import { CubeModel } from './CubeModel.js';
-
-// Static order definitions from rubiks-js source
-const CORNER_ORDER = ['URF', 'ULF', 'ULB', 'URB', 'DRF', 'DLF', 'DLB', 'DRB'];
-const EDGE_ORDER = ['UF', 'UL', 'UB', 'UR', 'FR', 'FL', 'BL', 'BR', 'DF', 'DL', 'DB', 'DR'];
-
-// Coordinate mapping for visualization
-// Coordinates match the visual grid positions
-const CORNER_SLOTS = [
- { id: 'URF', x: 1, y: 1, z: 1 },
- { id: 'ULF', x: -1, y: 1, z: 1 },
- { id: 'ULB', x: -1, y: 1, z: -1 },
- { id: 'URB', x: 1, y: 1, z: -1 },
- { id: 'DRF', x: 1, y: -1, z: 1 },
- { id: 'DLF', x: -1, y: -1, z: 1 },
- { id: 'DLB', x: -1, y: -1, z: -1 },
- { id: 'DRB', x: 1, y: -1, z: -1 }
-];
-
-const EDGE_SLOTS = [
- { id: 'UF', x: 0, y: 1, z: 1 },
- { id: 'UL', x: -1, y: 1, z: 0 },
- { id: 'UB', x: 0, y: 1, z: -1 },
- { id: 'UR', x: 1, y: 1, z: 0 },
- { id: 'FR', x: 1, y: 0, z: 1 },
- { id: 'FL', x: -1, y: 0, z: 1 },
- { id: 'BL', x: -1, y: 0, z: -1 },
- { id: 'BR', x: 1, y: 0, z: -1 },
- { id: 'DF', x: 0, y: -1, z: 1 },
- { id: 'DL', x: -1, y: -1, z: 0 },
- { id: 'DB', x: 0, y: -1, z: -1 },
- { id: 'DR', x: 1, y: -1, z: 0 }
-];
-
-const CENTERS = [
- { id: 'c0', x: 0, y: 1, z: 0, faces: { up: 'white' } },
- { id: 'c1', x: 0, y: -1, z: 0, faces: { down: 'yellow' } },
- { id: 'c2', x: 0, y: 0, z: 1, faces: { front: 'green' } },
- { id: 'c3', x: 0, y: 0, z: -1, faces: { back: 'blue' } },
- { id: 'c4', x: -1, y: 0, z: 0, faces: { left: 'orange' } },
- { id: 'c5', x: 1, y: 0, z: 0, faces: { right: 'red' } },
- { id: 'core', x: 0, y: 0, z: 0, faces: {} }
-];
-
-// Face mapping for pieces
-// Each piece (e.g. URF) has 3 faces. We need to map them to colors based on orientation.
-// Standard color scheme: U=white, D=yellow, F=green, B=blue, L=orange, R=red
-const FACE_COLORS = {
- U: 'white', D: 'yellow', F: 'green', B: 'blue', L: 'orange', R: 'red'
-};
-
-// Map piece name (e.g. 'URF') to its primary face keys
-const CORNER_FACES = {
- 'URF': ['up', 'right', 'front'],
- 'ULF': ['up', 'front', 'left'],
- 'ULB': ['up', 'left', 'back'],
- 'URB': ['up', 'back', 'right'],
- 'DRF': ['down', 'right', 'front'],
- 'DLF': ['down', 'left', 'front'],
- 'DLB': ['down', 'back', 'left'],
- 'DRB': ['down', 'right', 'back']
-};
-
-const EDGE_FACES = {
- 'UF': ['up', 'front'],
- 'UL': ['up', 'left'],
- 'UB': ['up', 'back'],
- 'UR': ['up', 'right'],
- 'FR': ['front', 'right'],
- 'FL': ['front', 'left'],
- 'BL': ['back', 'left'],
- 'BR': ['back', 'right'],
- 'DF': ['down', 'front'],
- 'DL': ['down', 'left'],
- 'DB': ['down', 'back'],
- 'DR': ['down', 'right']
-};
-
-// Map piece name to its solved colors
-const getCornerColors = (name) => {
- // URF -> white, red, green
- const map = {
- 'URF': ['white', 'red', 'green'],
- 'ULF': ['white', 'green', 'orange'],
- 'ULB': ['white', 'orange', 'blue'],
- 'URB': ['white', 'blue', 'red'],
- 'DRF': ['yellow', 'red', 'green'],
- 'DLF': ['yellow', 'orange', 'green'], // Adjusted to match DLF face order (D, L, F)
- 'DLB': ['yellow', 'blue', 'orange'], // Adjusted to match DLB face order (D, B, L)
- 'DRB': ['yellow', 'red', 'blue'] // Adjusted to match DRB face order (D, R, B)
- };
- return map[name];
-};
-
-const getEdgeColors = (name) => {
- const map = {
- 'UF': ['white', 'green'],
- 'UL': ['white', 'orange'],
- 'UB': ['white', 'blue'],
- 'UR': ['white', 'red'],
- 'FR': ['green', 'red'],
- 'FL': ['green', 'orange'],
- 'BL': ['blue', 'orange'],
- 'BR': ['blue', 'red'],
- 'DF': ['yellow', 'green'],
- 'DL': ['yellow', 'orange'],
- 'DB': ['yellow', 'blue'],
- 'DR': ['yellow', 'red']
- };
- return map[name];
-};
-
-export class RubiksJSModel {
- constructor() {
- this.state = new State(false); // trackCenters=false
- this.visual = new CubeModel();
- }
-
- reset() {
- this.state = new State(false);
- this.visual = new CubeModel();
- }
-
- rotateLayer(axis, index, dir) {
- let move = '';
- if (axis === 'y') {
- if (index === 1) move = dir === 1 ? "U'" : "U";
- else if (index === -1) move = dir === 1 ? "D'" : "D";
- }
- else if (axis === 'x') {
- if (index === 1) move = dir === 1 ? "R'" : "R";
- else if (index === -1) move = dir === 1 ? "L'" : "L";
- }
- else if (axis === 'z') {
- if (index === 1) move = dir === 1 ? "F'" : "F";
- else if (index === -1) move = dir === 1 ? "B'" : "B";
- }
-
- if (move) {
- console.log('[RubiksJSModel] Applying move:', move);
- try {
- this.state.applyTurn(move);
- console.log('[RubiksJSModel] Move applied successfully');
- } catch (e) {
- console.error('[RubiksJSModel] Failed to apply move:', move, e);
- }
-
- this.visual.rotateLayer(axis, index, dir);
- }
- }
-
- applyTurn(move) {
- if (!move) return;
- try {
- this.state.applyTurn(move);
- } catch (e) {
- console.error('[RubiksJSModel] Failed to apply direct move:', move, e);
- }
-
- this.visual.applyMove(move);
- }
-
- toCubies() {
- return this.visual.toCubies();
- }
-
- validate() {
- // State doesn't expose validate, but we can assume it's valid if using the library
- return { valid: true, errors: [] };
- }
-}
diff --git a/src/utils/solvers/BeginnerSolver.js b/src/utils/solvers/BeginnerSolver.js
new file mode 100644
index 0000000..fc925a1
--- /dev/null
+++ b/src/utils/solvers/BeginnerSolver.js
@@ -0,0 +1,376 @@
+import { DeepCube, MOVES } from "../DeepCube.js";
+
+export class BeginnerSolver {
+ constructor(cube) {
+ this.cube = cube.clone();
+ this.solution = [];
+ }
+
+ apply(moveStr) {
+ const moveArr = moveStr.split(" ").filter((m) => m);
+ for (const m of moveArr) {
+ if (!MOVES[m]) throw new Error(`Invalid move: ${m}`);
+ this.solution.push(m);
+ this.cube = this.cube.multiply(MOVES[m]);
+ }
+ }
+
+ solve() {
+ this.solution = [];
+
+ // Safety check - is it already solved?
+ if (this.isSolvedState(this.cube)) return [];
+
+ console.log("Starting Cross");
+ this.solveCross();
+ console.log("Starting F2L Corners");
+ this.solveF2LCorners();
+ console.log("Starting F2L Edges");
+ this.solveF2LEdges();
+ console.log("Starting Yellow Cross");
+ this.solveYellowCross();
+ console.log("Starting Yellow OLL");
+ this.orientYellowCorners();
+ console.log("Starting Yellow PLL");
+ this.permuteYellowCorners();
+ this.permuteYellowEdges();
+
+ // Optional: align U face
+ this.alignUFace();
+
+ // Collapse redundant moves like U U -> U2, R R' -> nothing
+ this.optimizeSolution();
+ return this.solution;
+ }
+
+ isSolvedState(state) {
+ for (let i = 0; i < 8; i++)
+ if (state.cp[i] !== i || state.co[i] !== 0) return false;
+ for (let i = 0; i < 12; i++)
+ if (state.ep[i] !== i || state.eo[i] !== 0) return false;
+ return true;
+ }
+
+ // Generalized Depth-Limited Search for localized piece-by-piece goals
+ solveGoal(condition, maxDepth, allowedMoves = Object.keys(MOVES)) {
+ if (condition(this.cube)) return true;
+
+ const opposing = { U: "D", D: "U", L: "R", R: "L", F: "B", B: "F" };
+
+ const dfs = (node, depth, lastMove) => {
+ if (depth === 0) return condition(node) ? [] : null;
+ const lastFace = lastMove ? lastMove[0] : null;
+
+ for (let m of allowedMoves) {
+ const face = m[0];
+ if (face === lastFace) continue;
+ if (opposing[face] === lastFace && face > lastFace) continue;
+
+ const nextNode = node.multiply(MOVES[m]);
+ if (depth === 1) {
+ if (condition(nextNode)) return [m];
+ } else {
+ const path = dfs(nextNode, depth - 1, m);
+ if (path) return [m, ...path];
+ }
+ }
+ return null;
+ };
+
+ for (let d = 1; d <= maxDepth; d++) {
+ const res = dfs(this.cube, d, "");
+ if (res) {
+ res.forEach((m) => this.apply(m));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ solveCross() {
+ // White Cross on D face
+ // Edge Pieces: DF(5), DR(4), DB(7), DL(6)
+ const targets = [5, 4, 7, 6];
+ for (let i = 0; i < targets.length; i++) {
+ const goal = (state) => {
+ for (let j = 0; j <= i; j++) {
+ const t = targets[j];
+ if (state.ep[t] !== t || state.eo[t] !== 0) return false;
+ }
+ return true;
+ };
+ if (!this.solveGoal(goal, 7)) throw new Error("Failed Cross depth 7");
+ }
+ }
+
+ solveF2LCorners() {
+ // DFR(4), DRB(7), DBL(6), DLF(5)
+ const corners = [4, 7, 6, 5];
+ for (let i = 0; i < corners.length; i++) {
+ const goal = (state) => {
+ // Must preserve cross
+ if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
+ if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
+ if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
+ if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
+
+ // Must preserve prior corners
+ for (let j = 0; j <= i; j++) {
+ const c = corners[j];
+ if (state.cp[c] !== c || state.co[c] !== 0) return false;
+ }
+ return true;
+ };
+ if (!this.solveGoal(goal, 8))
+ throw new Error("Failed F2L Corners depth 8");
+ }
+ }
+
+ solveF2LEdges() {
+ // FR(8), BR(11), BL(10), FL(9)
+ const edges = [8, 11, 10, 9];
+ const allowed = [
+ "U",
+ "U'",
+ "U2",
+ "R",
+ "R'",
+ "R2",
+ "F",
+ "F'",
+ "F2",
+ "L",
+ "L'",
+ "L2",
+ "B",
+ "B'",
+ "B2",
+ ]; // Avoid D moves
+
+ for (let i = 0; i < edges.length; i++) {
+ const goal = (state) => {
+ // Preserve cross
+ if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
+ if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
+ if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
+ if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
+ // Preserve all D corners
+ if (state.cp[4] !== 4 || state.co[4] !== 0) return false;
+ if (state.cp[7] !== 7 || state.co[7] !== 0) return false;
+ if (state.cp[6] !== 6 || state.co[6] !== 0) return false;
+ if (state.cp[5] !== 5 || state.co[5] !== 0) return false;
+
+ // Preserve prior edges
+ for (let j = 0; j <= i; j++) {
+ const e = edges[j];
+ if (state.ep[e] !== e || state.eo[e] !== 0) return false;
+ }
+ return true;
+ };
+ // Depth 10 is needed for inserting edge from bad positions
+ if (!this.solveGoal(goal, 10, allowed))
+ throw new Error("Failed F2L Edges depth 10");
+ }
+ }
+
+ // --- BEGIN CFOP LAST LAYER MACROS ---
+ solveYellowCross() {
+ // Find yellow cross edges: UR(0), UF(1), UL(2), UB(3)
+ // They just need orientation eo=0.
+ const getOrientedCount = () =>
+ [0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0).length;
+
+ let safetyCount = 0;
+ while (getOrientedCount() < 4 && safetyCount++ < 10) {
+ const oriented = [0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0);
+
+ if (oriented.length === 0) {
+ this.apply("F R U R' U' F'");
+ } else if (oriented.length === 2) {
+ // Line (opposite) or L-shape (adjacent)
+ const [a, b] = oriented;
+ if (Math.abs(a - b) === 2) {
+ // Line geometry (UR and UL, or UF and UB)
+ // To apply F R U R' U' F', the line must be horizontal.
+ // If line is UR(0) and UL(2), it's horizontal from F perspective.
+ if (a === 0 && b === 2) {
+ this.apply("F R U R' U' F'");
+ } else {
+ this.apply("U"); // turn line so it is UR/UL
+ this.apply("F R U R' U' F'");
+ }
+ } else {
+ // L-shape geometry
+ // Macro: F U R U' R' F' requires L to be at Back and Left (UB=3, UL=2)
+ if (oriented.includes(3) && oriented.includes(2)) {
+ this.apply("F U R U' R' F'");
+ } else {
+ this.apply("U");
+ }
+ }
+ }
+ }
+ }
+
+ orientYellowCorners() {
+ // Sune / Antisune to orient yellow corners (co=0)
+ // OLL for corners alone using the repetitive beginner algorithm (R' D' R D)
+ // Position the unsolved corner at URF (0) and repeat R' D' R D until co[0] === 0
+
+ let safetyCount = 0;
+ while (safetyCount++ < 20) {
+ let solvedCount = [0, 1, 2, 3].filter(
+ (i) => this.cube.co[i] === 0,
+ ).length;
+ if (solvedCount === 4) break;
+
+ if (this.cube.co[0] === 0) {
+ this.apply("U"); // Next
+ } else {
+ // Apply R' D' R D twice (1 cycle)
+ this.apply("R' D' R D R' D' R D");
+ }
+ }
+ }
+
+ permuteYellowCorners() {
+ // Beginner method: look for "headlights" (two corners on the same face with the same color targeting that face).
+ // Mathematically: two adjacent U corners whose permutation matches their distance.
+ // E.g., URF(0) and UBR(3). If their permuted values are also adjacent, they are headlights.
+
+ const hasHeadlightsOnBack = () => {
+ // Back corners are ULB(2) and UBR(3)
+ // To be headlights on the back, they must belong to the same face in their solved state.
+ // Wait, comparing corner colors mathematically:
+ // In a solved cube, the U face has 4 corners: 0, 1, 2, 3.
+ // The distance between cp[2] and cp[3] modulo 4 should be exactly 1 or 3 (adjacent).
+ // Actually, if they are correctly relative to EACH OTHER:
+ return (
+ (this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 3 ||
+ (this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1
+ );
+ };
+
+ // Simplified: Just keep applying the A-perm (headlight creator) until all 4 corners are relatively solved.
+ let safetyCount = 0;
+ while (safetyCount++ < 10) {
+ // Check if corners are relatively solved (i.e. 0->1->2->3 in order)
+ let c0 = this.cube.cp[0],
+ c1 = this.cube.cp[1],
+ c2 = this.cube.cp[2],
+ c3 = this.cube.cp[3];
+ if (
+ (c1 - c0 + 4) % 4 === 1 &&
+ (c2 - c1 + 4) % 4 === 1 &&
+ (c3 - c2 + 4) % 4 === 1
+ ) {
+ break; // All corners are cyclically ordered
+ }
+
+ // If Back corners are cyclically ordered (Headlights on Back)
+ if ((c2 - c3 + 4) % 4 === 1 || (c3 - c2 + 4) % 4 === 3) {
+ // Wait, order must be 3->0->1->2. So ULB(2) should be next after UBR(3).
+ // If cp[2] comes immediately after cp[3] cyclically:
+ if ((this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1) {
+ this.apply("R' F R' B2 R F' R' B2 R2");
+ } else {
+ this.apply("U");
+ }
+ } else {
+ // Find ANY headlights and put them on the back
+ if ((c1 - c2 + 4) % 4 === 1) {
+ this.apply("U");
+ continue;
+ }
+ if ((c0 - c1 + 4) % 4 === 1) {
+ this.apply("U2");
+ continue;
+ }
+ if ((c3 - c0 + 4) % 4 === 1) {
+ this.apply("U'");
+ continue;
+ }
+
+ // No headlights at all (diagonal swap), just apply the alg anywhere
+ this.apply("R' F R' B2 R F' R' B2 R2");
+ }
+ }
+ }
+
+ permuteYellowEdges() {
+ // Corners are properly ordered now. We just need to align them to the centers first.
+ while (this.cube.cp[0] !== 0) {
+ this.apply("U");
+ }
+
+ // Now corners are 100% solved. Check edges.
+ let safetyCount = 0;
+ while (safetyCount++ < 10) {
+ if (
+ this.cube.ep[0] === 0 &&
+ this.cube.ep[1] === 1 &&
+ this.cube.ep[2] === 2 &&
+ this.cube.ep[3] === 3
+ ) {
+ break; // fully solved!
+ }
+
+ // Check if one edge is solved. If so, put it at the BACK (UB=3)
+ if (this.cube.ep[3] === 3) {
+ // Apply U-perm
+ this.apply("R U' R U R U R U' R' U' R2");
+ } else if (this.cube.ep[0] === 0) {
+ this.apply("U"); // turn whole cube or just U... wait, standard alg rotates U.
+ // To keep corners solved but move edges, we'd have to do y rotations.
+ // Let's just use the U-perm and see if it solves.
+ // If UR(0) is solved, we want it at UB(3). So we do a U move, apply alg, do U'.
+ // Wait, applying U moves the corners! We can't do that.
+ // Standard solving often uses `y` cube rotations. Since our model only supports face moves,
+ // we can use the transposed algorithms.
+ // Alg for solved Right(0): F U' F U F U F U' F' U' F2
+ this.apply("F U' F U F U F U' F' U' F2");
+ } else if (this.cube.ep[1] === 1) {
+ this.apply("L U' L U L U L U' L' U' L2");
+ } else if (this.cube.ep[2] === 2) {
+ this.apply("B U' B U B U B U' B' U' B2");
+ } else {
+ // No edges solved. Apply U-perm from anywhere.
+ this.apply("R U' R U R U R U' R' U' R2");
+ }
+ }
+ }
+
+ alignUFace() {
+ while (this.cube.cp[0] !== 0) {
+ this.apply("U");
+ }
+ }
+
+ optimizeSolution() {
+ // A quick pass to cancel out redundant moves like U U' -> nothing, U U -> U2, U2 U -> U'
+ let stable = false;
+ while (!stable) {
+ stable = true;
+ for (let i = 0; i < this.solution.length - 1; i++) {
+ const a = this.solution[i];
+ const b = this.solution[i + 1];
+
+ if (a[0] === b[0]) {
+ // Same face! Let's sum the rotation.
+ const val = (m) => (m.includes("'") ? -1 : m.includes("2") ? 2 : 1);
+ let sum = (val(a) + val(b)) % 4;
+ if (sum < 0) sum += 4; // normalize to positive
+
+ this.solution.splice(i, 2); // remove both
+
+ if (sum === 1) this.solution.splice(i, 0, a[0]);
+ else if (sum === 2) this.solution.splice(i, 0, a[0] + "2");
+ else if (sum === 3) this.solution.splice(i, 0, a[0] + "'");
+
+ stable = false;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/utils/solvers/KociembaSolver.js b/src/utils/solvers/KociembaSolver.js
new file mode 100644
index 0000000..ae21062
--- /dev/null
+++ b/src/utils/solvers/KociembaSolver.js
@@ -0,0 +1,139 @@
+import Cube from "cubejs";
+
+// Initialize the core pruning tables on module load
+Cube.initSolver();
+import { DeepCube, CORNERS, EDGES } from "../DeepCube.js";
+
+export class KociembaSolver {
+ constructor(cube) {
+ this.cube = cube.clone();
+ }
+
+ // Convert DeepCube permutation/orientation to Kociemba facelet string
+ // Kociemba format: U1..U9 R1..R9 F1..F9 D1..D9 L1..L9 B1..B9
+ toFaceletString() {
+ // Array of 54 characters representing the 6 faces.
+ // 0..8 = U
+ // 9..17 = R
+ // 18..26 = F
+ // 27..35 = D
+ // 36..44 = L
+ // 45..53 = B
+
+ const f = new Array(54).fill(" ");
+
+ // Centers
+ f[4] = "U";
+ f[13] = "R";
+ f[22] = "F";
+ f[31] = "D";
+ f[40] = "L";
+ f[49] = "B";
+
+ // DeepCube to Kociemba mapping:
+ // Corners:
+ // 0: URF, 1: UFL, 2: ULB, 3: UBR, 4: DFR, 5: DLF, 6: DBL, 7: DRB
+ // Edges:
+ // 0: UR, 1: UF, 2: UL, 3: UB, 4: DR, 5: DF, 6: DL, 7: DB, 8: FR, 9: FL, 10: BL, 11: BR
+
+ const cornerColors = [
+ ["U", "R", "F"], // 0: URF
+ ["U", "F", "L"], // 1: UFL
+ ["U", "L", "B"], // 2: ULB
+ ["U", "B", "R"], // 3: UBR
+ ["D", "F", "R"], // 4: DFR
+ ["D", "L", "F"], // 5: DLF
+ ["D", "B", "L"], // 6: DBL
+ ["D", "R", "B"], // 7: DRB
+ ];
+
+ const cornerFacelets = [
+ [8, 9, 20], // URF (U9, R1, F3)
+ [6, 18, 38], // UFL (U7, F1, L3)
+ [0, 36, 47], // ULB (U1, L1, B3)
+ [2, 45, 11], // UBR (U3, B1, R3)
+ [29, 26, 15], // DFR (D3, F9, R7)
+ [27, 44, 24], // DLF (D1, L9, F7)
+ [33, 53, 42], // DBL (D7, B9, L7)
+ [35, 17, 51], // DRB (D9, R9, B7)
+ ];
+
+ for (let i = 0; i < 8; i++) {
+ const perm = this.cube.cp[i];
+ const ori = this.cube.co[i];
+
+ // The physical piece at position `i` is `perm`.
+ // Its colors are cornerColors[perm].
+ // Because of orientation, the colors are shifted.
+ // If ori=0, U/D color is on U/D face.
+ // If ori=1, U/D color is twisted clockwise.
+ // If ori=2, U/D color is twisted counter-clockwise.
+
+ const c0 = cornerColors[perm][(0 - ori + 3) % 3];
+ const c1 = cornerColors[perm][(1 - ori + 3) % 3];
+ const c2 = cornerColors[perm][(2 - ori + 3) % 3];
+
+ f[cornerFacelets[i][0]] = c0;
+ f[cornerFacelets[i][1]] = c1;
+ f[cornerFacelets[i][2]] = c2;
+ }
+
+ const edgeColors = [
+ ["U", "R"], // 0: UR
+ ["U", "F"], // 1: UF
+ ["U", "L"], // 2: UL
+ ["U", "B"], // 3: UB
+ ["D", "R"], // 4: DR
+ ["D", "F"], // 5: DF
+ ["D", "L"], // 6: DL
+ ["D", "B"], // 7: DB
+ ["F", "R"], // 8: FR
+ ["F", "L"], // 9: FL
+ ["B", "L"], // 10: BL
+ ["B", "R"], // 11: BR
+ ];
+
+ const edgeFacelets = [
+ [5, 10], // UR (U6, R2)
+ [7, 19], // UF (U8, F2)
+ [3, 37], // UL (U4, L2)
+ [1, 46], // UB (U2, B2)
+ [32, 16], // DR (D6, R8)
+ [28, 25], // DF (D2, F8)
+ [30, 43], // DL (D4, L8)
+ [34, 52], // DB (D8, B8)
+ [23, 12], // FR (F6, R4)
+ [21, 41], // FL (F4, L6)
+ [50, 39], // BL (B6, L4)
+ [48, 14], // BR (B4, R6)
+ ];
+
+ for (let i = 0; i < 12; i++) {
+ const perm = this.cube.ep[i];
+ const ori = this.cube.eo[i];
+
+ const e0 = edgeColors[perm][(0 + ori) % 2];
+ const e1 = edgeColors[perm][(1 + ori) % 2];
+
+ f[edgeFacelets[i][0]] = e0;
+ f[edgeFacelets[i][1]] = e1;
+ }
+
+ return f.join("");
+ }
+
+ solve() {
+ const faceletStr = this.toFaceletString();
+ try {
+ const cube = Cube.fromString(faceletStr);
+ if (cube.isSolved()) return [];
+ const solution = cube.solve();
+ if (!solution) return [];
+ return solution.split(" ").filter((m) => m);
+ } catch (e) {
+ throw new Error(
+ `Kociemba Solve Failed: ${e.message} \nFacelet: ${faceletStr}`,
+ );
+ }
+ }
+}
diff --git a/src/workers/Cube.worker.js b/src/workers/Cube.worker.js
index d4d82ef..3b9d58a 100644
--- a/src/workers/Cube.worker.js
+++ b/src/workers/Cube.worker.js
@@ -1,21 +1,22 @@
-import { RubiksJSModel } from '../utils/RubiksJSModel.js';
+import { RubiksJSModel } from "../utils/CubeLogicAdapter.js";
const cube = new RubiksJSModel();
+
// Helper to send state update
const sendUpdate = () => {
try {
const cubies = cube.toCubies();
// console.log('[Worker] Sending update with cubies:', cubies.length);
postMessage({
- type: 'STATE_UPDATE',
+ type: "STATE_UPDATE",
payload: {
- cubies
- }
+ cubies,
+ },
});
} catch (e) {
- console.error('[Worker] Error generating cubies:', e);
- postMessage({ type: 'ERROR', payload: e.message });
+ console.error("[Worker] Error generating cubies:", e);
+ postMessage({ type: "ERROR", payload: e.message });
}
};
@@ -23,31 +24,31 @@ self.onmessage = (e) => {
const { type, payload } = e.data;
switch (type) {
- case 'INIT':
- case 'RESET':
+ case "INIT":
+ case "RESET":
cube.reset();
sendUpdate();
break;
- case 'ROTATE_LAYER': {
- const { axis, index, direction } = payload;
- cube.rotateLayer(axis, index, direction);
+ case "ROTATE_LAYER": {
+ const { axis, index, direction, steps = 1 } = payload;
+ cube.rotateLayer(axis, index, direction, steps);
sendUpdate();
break;
}
- case 'TURN': {
+ case "TURN": {
const { move } = payload;
cube.applyTurn(move);
sendUpdate();
break;
}
- case 'VALIDATE':
+ case "VALIDATE":
const validation = cube.validate();
postMessage({
- type: 'VALIDATION_RESULT',
- payload: { valid: validation.valid, errors: validation.errors }
+ type: "VALIDATION_RESULT",
+ payload: { valid: validation.valid, errors: validation.errors },
});
break;
}
diff --git a/test/cube_integrity.test.js b/test/cube_integrity.test.js
index a90703b..c998335 100644
--- a/test/cube_integrity.test.js
+++ b/test/cube_integrity.test.js
@@ -1,8 +1,7 @@
+import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
+import assert from "assert";
-import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
-import assert from 'assert';
-
-console.log('Running Cube Integrity Tests...');
+console.log("Running Cube Integrity Tests...");
const cube = new Cube();
@@ -15,11 +14,11 @@ const countColors = () => {
[COLORS.RED]: 0,
[COLORS.GREEN]: 0,
[COLORS.BLUE]: 0,
- [COLORS.BLACK]: 0 // Should be ignored or internal
+ [COLORS.BLACK]: 0, // Should be ignored or internal
};
- cube.cubies.forEach(cubie => {
- Object.values(cubie.faces).forEach(color => {
+ cube.cubies.forEach((cubie) => {
+ Object.values(cubie.faces).forEach((color) => {
if (counts[color] !== undefined) {
counts[color]++;
}
@@ -35,13 +34,13 @@ const verifyCounts = (counts) => {
// 9 * 6 = 54 colored stickers.
// 27 cubies * 6 faces = 162 total faces.
// 162 - 54 = 108 black faces (internal).
-
- assert.strictEqual(counts[COLORS.WHITE], 9, 'White count should be 9');
- assert.strictEqual(counts[COLORS.YELLOW], 9, 'Yellow count should be 9');
- assert.strictEqual(counts[COLORS.ORANGE], 9, 'Orange count should be 9');
- assert.strictEqual(counts[COLORS.RED], 9, 'Red count should be 9');
- assert.strictEqual(counts[COLORS.GREEN], 9, 'Green count should be 9');
- assert.strictEqual(counts[COLORS.BLUE], 9, 'Blue count should be 9');
+
+ assert.strictEqual(counts[COLORS.WHITE], 9, "White count should be 9");
+ assert.strictEqual(counts[COLORS.YELLOW], 9, "Yellow count should be 9");
+ assert.strictEqual(counts[COLORS.ORANGE], 9, "Orange count should be 9");
+ assert.strictEqual(counts[COLORS.RED], 9, "Red count should be 9");
+ assert.strictEqual(counts[COLORS.GREEN], 9, "Green count should be 9");
+ assert.strictEqual(counts[COLORS.BLUE], 9, "Blue count should be 9");
};
// Helper: Verify piece integrity
@@ -55,19 +54,24 @@ const verifyPieceTypes = () => {
let centers = 0;
let cores = 0;
- cube.cubies.forEach(cubie => {
- const coloredFaces = Object.values(cubie.faces).filter(c => c !== COLORS.BLACK).length;
+ cube.cubies.forEach((cubie) => {
+ const coloredFaces = Object.values(cubie.faces).filter(
+ (c) => c !== COLORS.BLACK,
+ ).length;
if (coloredFaces === 3) corners++;
else if (coloredFaces === 2) edges++;
else if (coloredFaces === 1) centers++;
else if (coloredFaces === 0) cores++;
- else assert.fail(`Invalid cubie with ${coloredFaces} colors at (${cubie.x},${cubie.y},${cubie.z})`);
+ else
+ assert.fail(
+ `Invalid cubie with ${coloredFaces} colors at (${cubie.x},${cubie.y},${cubie.z})`,
+ );
});
- assert.strictEqual(corners, 8, 'Should have 8 corners');
- assert.strictEqual(edges, 12, 'Should have 12 edges');
- assert.strictEqual(centers, 6, 'Should have 6 centers');
- assert.strictEqual(cores, 1, 'Should have 1 core');
+ assert.strictEqual(corners, 8, "Should have 8 corners");
+ assert.strictEqual(edges, 12, "Should have 12 edges");
+ assert.strictEqual(centers, 6, "Should have 6 centers");
+ assert.strictEqual(cores, 1, "Should have 1 core");
};
// Helper: Verify specific relative positions of centers (they never change relative to each other)
@@ -75,13 +79,15 @@ const verifyPieceTypes = () => {
// Front (Green) opposite Back (Blue)
// Left (Orange) opposite Right (Red)
const verifyCenters = () => {
- const centers = cube.cubies.filter(c =>
- Object.values(c.faces).filter(f => f !== COLORS.BLACK).length === 1
+ const centers = cube.cubies.filter(
+ (c) =>
+ Object.values(c.faces).filter((f) => f !== COLORS.BLACK).length === 1,
);
-
+
// Find center by color
- const findCenter = (color) => centers.find(c => Object.values(c.faces).includes(color));
-
+ const findCenter = (color) =>
+ centers.find((c) => Object.values(c.faces).includes(color));
+
const white = findCenter(COLORS.WHITE);
const yellow = findCenter(COLORS.YELLOW);
const green = findCenter(COLORS.GREEN);
@@ -92,44 +98,43 @@ const verifyCenters = () => {
// Check opposites
// Distance between opposites should be 2 (e.g. y=1 and y=-1)
// And they should be on same axis
-
+
// Note: After rotations, x/y/z coordinates change.
// But relative vectors should hold?
// Actually, centers DO rotate around the core.
// But White is always opposite Yellow.
// So vector(White) + vector(Yellow) == (0,0,0).
-
+
const checkOpposite = (c1, c2, name) => {
assert.strictEqual(c1.x + c2.x, 0, `${name} X mismatch`);
assert.strictEqual(c1.y + c2.y, 0, `${name} Y mismatch`);
assert.strictEqual(c1.z + c2.z, 0, `${name} Z mismatch`);
};
- checkOpposite(white, yellow, 'White-Yellow');
- checkOpposite(green, blue, 'Green-Blue');
- checkOpposite(orange, red, 'Orange-Red');
+ checkOpposite(white, yellow, "White-Yellow");
+ checkOpposite(green, blue, "Green-Blue");
+ checkOpposite(orange, red, "Orange-Red");
};
-
// --- Test Execution ---
// 1. Initial State
-console.log('Test 1: Initial State Integrity');
+console.log("Test 1: Initial State Integrity");
verifyCounts(countColors());
verifyPieceTypes();
verifyCenters();
-console.log('PASS Initial State');
+console.log("PASS Initial State");
// 2. Single Rotation (R)
-console.log('Test 2: Single Rotation (R)');
-cube.rotateLayer('x', 1, -1); // R
+console.log("Test 2: Single Rotation (R)");
+cube.rotateLayer("x", 1, -1); // R
verifyCounts(countColors());
verifyPieceTypes();
verifyCenters();
-console.log('PASS Single Rotation');
+console.log("PASS Single Rotation");
// 3. Multiple Rotations (R U R' U')
-console.log('Test 3: Sexy Move (R U R\' U\')');
+console.log("Test 3: Sexy Move (R U R' U')");
cube.reset();
cube.move("R");
cube.move("U");
@@ -138,12 +143,12 @@ cube.move("U'");
verifyCounts(countColors());
verifyPieceTypes();
verifyCenters();
-console.log('PASS Sexy Move');
+console.log("PASS Sexy Move");
// 4. Random Rotations (Fuzzing)
-console.log('Test 4: 100 Random Moves');
+console.log("Test 4: 100 Random Moves");
cube.reset();
-const axes = ['x', 'y', 'z'];
+const axes = ["x", "y", "z"];
const indices = [-1, 0, 1];
const dirs = [1, -1];
@@ -156,6 +161,6 @@ for (let i = 0; i < 100; i++) {
verifyCounts(countColors());
verifyPieceTypes();
verifyCenters();
-console.log('PASS 100 Random Moves');
+console.log("PASS 100 Random Moves");
-console.log('ALL INTEGRITY TESTS PASSED');
+console.log("ALL INTEGRITY TESTS PASSED");
diff --git a/test/cube_logic.test.js b/test/cube_logic.test.js
index 97134a9..66f5fb3 100644
--- a/test/cube_logic.test.js
+++ b/test/cube_logic.test.js
@@ -1,32 +1,33 @@
+import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
+import assert from "assert";
-import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
-import assert from 'assert';
-
-console.log('Running Cube Logic Tests...');
+console.log("Running Cube Logic Tests...");
const cube = new Cube();
// Helper to check a specific face color at a position
const checkFace = (x, y, z, face, expectedColor, message) => {
- const cubie = cube.cubies.find(c => c.x === x && c.y === y && c.z === z);
+ const cubie = cube.cubies.find((c) => c.x === x && c.y === y && c.z === z);
if (!cubie) {
console.error(`Cubie not found at ${x}, ${y}, ${z}`);
return false;
}
const color = cubie.faces[face];
if (color !== expectedColor) {
- console.error(`FAIL: ${message}. Expected ${expectedColor} at ${face} of (${x},${y},${z}), got ${color}`);
+ console.error(
+ `FAIL: ${message}. Expected ${expectedColor} at ${face} of (${x},${y},${z}), got ${color}`,
+ );
return false;
}
return true;
};
// Test 1: Initial State
-console.log('Test 1: Initial State');
+console.log("Test 1: Initial State");
// Top-Front-Right corner (1, 1, 1) should have Up=White, Front=Green, Right=Red
-checkFace(1, 1, 1, FACES.UP, COLORS.WHITE, 'Initial Top-Right-Front UP');
-checkFace(1, 1, 1, FACES.FRONT, COLORS.GREEN, 'Initial Top-Right-Front FRONT');
-checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, 'Initial Top-Right-Front RIGHT');
+checkFace(1, 1, 1, FACES.UP, COLORS.WHITE, "Initial Top-Right-Front UP");
+checkFace(1, 1, 1, FACES.FRONT, COLORS.GREEN, "Initial Top-Right-Front FRONT");
+checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, "Initial Top-Right-Front RIGHT");
// Test 2: Rotate Right Face (R) -> Axis X, index 1, direction -1 (based on previous mapping)
// Wait, let's test `rotateLayer` directly first with axis 'x'.
@@ -42,13 +43,20 @@ checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, 'Initial Top-Right-Front RIGHT');
// The UP face of the cubie now points FRONT.
// So the cubie at (1, -1, 1) should have FRONT = WHITE.
-console.log('Test 2: Rotate X Axis +90 (Right Layer)');
-cube.rotateLayer('x', 1, 1);
+console.log("Test 2: Rotate X Axis +90 (Right Layer)");
+cube.rotateLayer("x", 1, 1);
// Cubie originally at (1, 1, 1) [White Up] moves to (1, -1, 1).
// Check (1, -1, 1).
// Its Front face should be White.
-const result1 = checkFace(1, -1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old Up(White) should be on Front');
+const result1 = checkFace(
+ 1,
+ -1,
+ 1,
+ FACES.FRONT,
+ COLORS.WHITE,
+ "After X+90: Old Up(White) should be on Front",
+);
// Cubie originally at (1, 1, -1) [Blue Back, White Up] (Top-Back-Right)
// (1, 1, -1) -> (1, 1, 1). (Top-Front-Right).
@@ -68,29 +76,42 @@ const result1 = checkFace(1, -1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old
// Top (Y+) rotates to Front (Z+)?
// Yes.
// So the cubie at (1, 1, 1) (new position) should have FRONT = WHITE.
-const result2 = checkFace(1, 1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old Top-Back Up(White) should be on Front');
+const result2 = checkFace(
+ 1,
+ 1,
+ 1,
+ FACES.FRONT,
+ COLORS.WHITE,
+ "After X+90: Old Top-Back Up(White) should be on Front",
+);
if (result1 && result2) {
- console.log('PASS: X Axis Rotation Logic seems correct (if fixed)');
+ console.log("PASS: X Axis Rotation Logic seems correct (if fixed)");
} else {
- console.log('FAIL: X Axis Rotation Logic is broken');
+ console.log("FAIL: X Axis Rotation Logic is broken");
}
// Reset for Y test
cube.reset();
-console.log('Test 3: Rotate Y Axis +90 (Top Layer)');
+console.log("Test 3: Rotate Y Axis +90 (Top Layer)");
// Top Layer (y=1).
// Rotate Y+ (direction 1).
// Front (z=1) -> Right (x=1).
// Cubie at (0, 1, 1) (Front-Top-Center) [Green Front, White Up].
// Moves to (1, 1, 0) (Right-Top-Center).
// Its Front Face (Green) should move to Right Face.
-cube.rotateLayer('y', 1, 1);
-const resultY = checkFace(1, 1, 0, FACES.RIGHT, COLORS.GREEN, 'After Y+90: Old Front(Green) should be on Right');
+cube.rotateLayer("y", 1, 1);
+const resultY = checkFace(
+ 1,
+ 1,
+ 0,
+ FACES.RIGHT,
+ COLORS.GREEN,
+ "After Y+90: Old Front(Green) should be on Right",
+);
if (resultY) {
- console.log('PASS: Y Axis Rotation Logic seems correct');
+ console.log("PASS: Y Axis Rotation Logic seems correct");
} else {
- console.log('FAIL: Y Axis Rotation Logic is broken');
+ console.log("FAIL: Y Axis Rotation Logic is broken");
}
-
diff --git a/test/cube_matrix.test.js b/test/cube_matrix.test.js
index 2ed1c36..9bb9d38 100644
--- a/test/cube_matrix.test.js
+++ b/test/cube_matrix.test.js
@@ -1,19 +1,20 @@
+import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
+import assert from "assert";
-import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
-import assert from 'assert';
-
-console.log('Running Cube Matrix Rotation Tests...');
+console.log("Running Cube Matrix Rotation Tests...");
const cube = new Cube();
// Helper to check position and face
const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => {
- const cubie = cube.cubies.find(c => c.x === newX && c.y === newY && c.z === newZ);
+ const cubie = cube.cubies.find(
+ (c) => c.x === newX && c.y === newY && c.z === newZ,
+ );
if (!cubie) {
console.error(`FAIL: Cubie not found at ${newX}, ${newY}, ${newZ}`);
return false;
}
-
+
// Verify it's the correct original cubie (tracking ID would be better, but position logic is enough if unique)
// Let's assume we track a specific cubie.
return true;
@@ -24,14 +25,14 @@ const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => {
// Top-Left (x=-1, y=1) -> Top-Right (x=1, y=1)?
// Physical CW (Z-Axis): Up -> Right.
// Top-Middle (0, 1) -> Right-Middle (1, 0).
-console.log('Test 1: Z-Axis CW (Front)');
+console.log("Test 1: Z-Axis CW (Front)");
cube.reset();
// Find Top-Middle of Front Face: (0, 1, 1). White Up, Green Front.
-const topMid = cube.cubies.find(c => c.x === 0 && c.y === 1 && c.z === 1);
+const topMid = cube.cubies.find((c) => c.x === 0 && c.y === 1 && c.z === 1);
assert.strictEqual(topMid.faces[FACES.UP], COLORS.WHITE);
assert.strictEqual(topMid.faces[FACES.FRONT], COLORS.GREEN);
-cube.rotateLayer('z', 1, -1); // CW (direction -1 in move(), but rotateLayer takes direction. Standard move F is direction -1?)
+cube.rotateLayer("z", 1, -1); // CW (direction -1 in move(), but rotateLayer takes direction. Standard move F is direction -1?)
// move('F') calls rotateLayer('z', 1, -1).
// So let's test rotateLayer('z', 1, -1).
@@ -40,27 +41,26 @@ cube.rotateLayer('z', 1, -1); // CW (direction -1 in move(), but rotateLayer tak
// Z-Axis CW: Up -> Right.
// So new pos should have Right=White.
// Old Front (Green) stays Front.
-const newPos = cube.cubies.find(c => c.id === topMid.id);
+const newPos = cube.cubies.find((c) => c.id === topMid.id);
console.log(`Moved to: (${newPos.x}, ${newPos.y}, ${newPos.z})`);
assert.strictEqual(newPos.x, 1);
assert.strictEqual(newPos.y, 0);
assert.strictEqual(newPos.z, 1);
assert.strictEqual(newPos.faces[FACES.RIGHT], COLORS.WHITE);
assert.strictEqual(newPos.faces[FACES.FRONT], COLORS.GREEN);
-console.log('PASS Z-Axis CW');
-
+console.log("PASS Z-Axis CW");
// Test 2: X-Axis Rotation (Right Face)
// Right Face is x=1.
// Top-Front (1, 1, 1) -> Top-Back (1, 1, -1)?
// Physical CW (X-Axis): Up -> Front.
// Top-Middle (1, 1, 0) -> Front-Middle (1, 0, 1).
-console.log('Test 2: X-Axis CW (Right)');
+console.log("Test 2: X-Axis CW (Right)");
cube.reset();
// Find Top-Middle of Right Face: (1, 1, 0). White Up, Red Right.
-const rightTop = cube.cubies.find(c => c.x === 1 && c.y === 1 && c.z === 0);
+const rightTop = cube.cubies.find((c) => c.x === 1 && c.y === 1 && c.z === 0);
-cube.rotateLayer('x', 1, -1); // CW (direction -1 for R in move()?)
+cube.rotateLayer("x", 1, -1); // CW (direction -1 for R in move()?)
// move('R') calls rotateLayer('x', 1, -1).
// So let's test -1.
@@ -69,15 +69,14 @@ cube.rotateLayer('x', 1, -1); // CW (direction -1 for R in move()?)
// X-Axis CW (Right Face): Up -> Back.
// So new pos should have Back=White.
// Old Right (Red) stays Right.
-const newRightPos = cube.cubies.find(c => c.id === rightTop.id);
+const newRightPos = cube.cubies.find((c) => c.id === rightTop.id);
console.log(`Moved to: (${newRightPos.x}, ${newRightPos.y}, ${newRightPos.z})`);
assert.strictEqual(newRightPos.x, 1);
assert.strictEqual(newRightPos.y, 0);
assert.strictEqual(newRightPos.z, -1);
assert.strictEqual(newRightPos.faces[FACES.BACK], COLORS.WHITE);
assert.strictEqual(newRightPos.faces[FACES.RIGHT], COLORS.RED);
-console.log('PASS X-Axis CW');
-
+console.log("PASS X-Axis CW");
// Test 3: Y-Axis Rotation (Up Face)
// Up Face is y=1.
@@ -86,24 +85,23 @@ console.log('PASS X-Axis CW');
// Wait. move('U') calls rotateLayer('y', 1, -1).
// Standard U is CW. Y-Axis direction?
// move('U'): dir = -1.
-console.log('Test 3: Y-Axis CW (Up)');
+console.log("Test 3: Y-Axis CW (Up)");
cube.reset();
// Find Front-Middle of Up Face: (0, 1, 1). Green Front, White Up.
-const upFront = cube.cubies.find(c => c.x === 0 && c.y === 1 && c.z === 1);
+const upFront = cube.cubies.find((c) => c.x === 0 && c.y === 1 && c.z === 1);
-cube.rotateLayer('y', 1, -1); // CW (direction -1).
+cube.rotateLayer("y", 1, -1); // CW (direction -1).
// Expect: (0, 1, 1) -> (-1, 1, 0). (Left-Middle).
// Faces: Old Front (Green) becomes Left?
// Y-Axis CW (U): Front -> Left.
// So new pos should have Left=Green.
// Old Up (White) stays Up.
-const newUpPos = cube.cubies.find(c => c.id === upFront.id);
+const newUpPos = cube.cubies.find((c) => c.id === upFront.id);
console.log(`Moved to: (${newUpPos.x}, ${newUpPos.y}, ${newUpPos.z})`);
assert.strictEqual(newUpPos.x, -1);
assert.strictEqual(newUpPos.y, 1);
assert.strictEqual(newUpPos.z, 0);
assert.strictEqual(newUpPos.faces[FACES.LEFT], COLORS.GREEN);
assert.strictEqual(newUpPos.faces[FACES.UP], COLORS.WHITE);
-console.log('PASS Y-Axis CW');
-
+console.log("PASS Y-Axis CW");
diff --git a/test/debug_kociemba.js b/test/debug_kociemba.js
new file mode 100644
index 0000000..72724e5
--- /dev/null
+++ b/test/debug_kociemba.js
@@ -0,0 +1,24 @@
+import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
+import { KociembaSolver } from "../src/utils/solvers/KociembaSolver.js";
+
+let cube = new DeepCube();
+
+const faceletStart = new KociembaSolver(cube).toFaceletString();
+console.log("Solved Facelet:");
+console.log(faceletStart);
+
+cube = cube.multiply(MOVES["R"]);
+const solverR = new KociembaSolver(cube);
+const faceletR = solverR.toFaceletString();
+console.log("Facelet after R:");
+console.log(faceletR);
+
+["U", "D", "R", "L", "F", "B"].forEach((m) => {
+ let c = new DeepCube().multiply(MOVES[m]);
+ let solver = new KociembaSolver(c);
+ try {
+ console.log(`Solution for ${m}:`, solver.solve().join(" "));
+ } catch (e) {
+ console.log(`Error on ${m}:`, e.message);
+ }
+});
diff --git a/test/generate_math.js b/test/generate_math.js
new file mode 100644
index 0000000..2d97ef2
--- /dev/null
+++ b/test/generate_math.js
@@ -0,0 +1,197 @@
+const C = ["URF", "UFL", "ULB", "UBR", "DFR", "DLF", "DBL", "DRB"];
+const E = [
+ "UR",
+ "UF",
+ "UL",
+ "UB",
+ "DR",
+ "DF",
+ "DL",
+ "DB",
+ "FR",
+ "FL",
+ "BL",
+ "BR",
+];
+
+// Define physical coordinates for all 6 center stickers
+const faces = {
+ U: [0, 1, 0],
+ D: [0, -1, 0],
+ R: [1, 0, 0],
+ L: [-1, 0, 0],
+ F: [0, 0, 1],
+ B: [0, 0, -1],
+};
+
+// 8 corners, each with 3 stickers
+// URF corner has stickers pointing U, R, F
+const cornerStickers = [
+ ["U", "R", "F"],
+ ["U", "F", "L"],
+ ["U", "L", "B"],
+ ["U", "B", "R"],
+ ["D", "F", "R"],
+ ["D", "L", "F"],
+ ["D", "B", "L"],
+ ["D", "R", "B"],
+];
+
+// 12 edges, each with 2 stickers
+const edgeStickers = [
+ ["U", "R"],
+ ["U", "F"],
+ ["U", "L"],
+ ["U", "B"],
+ ["D", "R"],
+ ["D", "F"],
+ ["D", "L"],
+ ["D", "B"],
+ ["F", "R"],
+ ["F", "L"],
+ ["B", "L"],
+ ["B", "R"],
+];
+
+// Rotate a 3D vector around an axis by 90 deg clockwise looking at the face
+function rotate(vec, axis) {
+ let [x, y, z] = vec;
+ // Holding the face and turning clockwise:
+ // U (Y+): Back(-Z) -> Right(+X) -> Front(+Z) -> Left(-X) -> Back(-Z)
+ // So X becomes Z, Z becomes -X
+ // Let's test UBR (X=1, Z=-1).
+ // Clockwise: UBR(TopRight) -> URF(BottomRight) -> UFL(BottomLeft) -> ULB(TopLeft).
+ // UBR (1,-1) -> URF (1,1). We need X'=1, Z'=1 from X=1, Z=-1.
+ // Formula for X'=1, Z'=1: X' = -Z, Z' = X.
+ // Let's try URF(1,1) -> UFL(-1,1): X' = -1, Z' = 1. matches X'=-Z, Z'=X.
+ // So U is [-z, y, x]
+ // D (Y-): Looking from bottom: Front(+Z) -> Right(+X) -> Back(-Z) -> Left(-X)
+ // So Front(Z=1) -> Right(X=1). Z'= -X? Yes. X'=Z.
+ // So D is [z, y, -x]
+ // R (X+): Up(+Y) -> Back(-Z) -> Down(-Y) -> Front(+Z)
+ // So Up(Y=1) -> Back(Z=-1). Y'= -Z? Yes. Z'=Y.
+ // So R is [x, -z, y]
+ // L (X-): Up(+Y) -> Front(+Z) -> Down(-Y) -> Back(-Z)
+ // So Up(Y=1) -> Front(Z=1). Y'= Z. Z'= -Y.
+ // So L is [x, z, -y]
+ // F (Z+): Up(+Y) -> Right(+X) -> Down(-Y) -> Left(-X)
+ // So Up(Y=1) -> Right(X=1). X'=Y. Y'=-X.
+ // So F is [y, -x, z]
+ // B (Z-): Up(+Y) -> Left(-X) -> Down(-Y) -> Right(+X)
+ // So Up(Y=1) -> Left(X=-1). X'=-Y. Y'=X.
+ // So B is [-y, x, z]
+
+ if (axis === "U") return [-z, y, x];
+ if (axis === "D") return [z, y, -x];
+ if (axis === "R") return [x, z, -y];
+ if (axis === "L") return [x, -z, y];
+ if (axis === "F") return [y, -x, z];
+ if (axis === "B") return [-y, x, z];
+}
+
+// Map a rotated vector back to a face name
+function vecToFace(vec) {
+ for (let f in faces) {
+ if (
+ faces[f][0] === vec[0] &&
+ faces[f][1] === vec[1] &&
+ faces[f][2] === vec[2]
+ )
+ return f;
+ }
+}
+
+function generateMove(axis) {
+ let cp = [],
+ co = [],
+ ep = [],
+ eo = [];
+
+ // CORNERS
+ for (let c = 0; c < 8; c++) {
+ if (!cornerStickers[c].includes(axis)) {
+ cp[c] = c;
+ co[c] = 0;
+ continue;
+ }
+
+ let pos = [0, 0, 0];
+ cornerStickers[c].forEach((f) => {
+ pos[0] += faces[f][0];
+ pos[1] += faces[f][1];
+ pos[2] += faces[f][2];
+ });
+ let newPos = rotate(pos, axis);
+
+ let targetC = -1;
+ for (let i = 0; i < 8; i++) {
+ let p2 = [0, 0, 0];
+ cornerStickers[i].forEach((f) => {
+ p2[0] += faces[f][0];
+ p2[1] += faces[f][1];
+ p2[2] += faces[f][2];
+ });
+ if (p2[0] === newPos[0] && p2[1] === newPos[1] && p2[2] === newPos[2])
+ targetC = i;
+ }
+
+ cp[targetC] = c;
+
+ let rotatedStickers = cornerStickers[c].map((f) =>
+ vecToFace(rotate(faces[f], axis)),
+ );
+ let ori = cornerStickers[targetC].indexOf(rotatedStickers[0]);
+ co[targetC] = ori;
+ }
+
+ // EDGES
+ for (let e = 0; e < 12; e++) {
+ if (!edgeStickers[e].includes(axis)) {
+ ep[e] = e;
+ eo[e] = 0;
+ continue;
+ }
+
+ let pos = [0, 0, 0];
+ edgeStickers[e].forEach((f) => {
+ pos[0] += faces[f][0];
+ pos[1] += faces[f][1];
+ pos[2] += faces[f][2];
+ });
+ let newPos = rotate(pos, axis);
+
+ let targetE = -1;
+ for (let i = 0; i < 12; i++) {
+ let p2 = [0, 0, 0];
+ edgeStickers[i].forEach((f) => {
+ p2[0] += faces[f][0];
+ p2[1] += faces[f][1];
+ p2[2] += faces[f][2];
+ });
+ if (p2[0] === newPos[0] && p2[1] === newPos[1] && p2[2] === newPos[2])
+ targetE = i;
+ }
+
+ ep[targetE] = e;
+
+ let rotatedStickers = edgeStickers[e].map((f) =>
+ vecToFace(rotate(faces[f], axis)),
+ );
+ let primarySticker = rotatedStickers[0];
+ let ori = primarySticker === edgeStickers[targetE][0] ? 0 : 1;
+ eo[targetE] = ori;
+ }
+
+ return { cp, co, ep, eo };
+}
+
+const moves = ["U", "R", "F", "D", "L", "B"];
+moves.forEach((m) => {
+ const res = generateMove(m);
+ console.log(`MOVES['${m}'] = new DeepCube(
+ [${res.cp.map((e) => `CORNERS.${C[e]}`).join(", ")}],
+ [${res.co.join(", ")}],
+ [${res.ep.map((e) => `EDGES.${E[e]}`).join(", ")}],
+ [${res.eo.join(", ")}]
+)`);
+});
diff --git a/test/math_output.txt b/test/math_output.txt
new file mode 100644
index 0000000..d4ed240
--- /dev/null
+++ b/test/math_output.txt
@@ -0,0 +1,36 @@
+MOVES['U'] = new DeepCube(
+ [CORNERS.UFL, CORNERS.ULB, CORNERS.UBR, CORNERS.URF, CORNERS.DFR, CORNERS.DLF, CORNERS.DBL, CORNERS.DRB],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [EDGES.UF, EDGES.UL, EDGES.UB, EDGES.UR, EDGES.DR, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.FR, EDGES.FL, EDGES.BL, EDGES.BR],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+)
+MOVES['R'] = new DeepCube(
+ [CORNERS.DFR, CORNERS.UFL, CORNERS.ULB, CORNERS.URF, CORNERS.DRB, CORNERS.DLF, CORNERS.DBL, CORNERS.UBR],
+ [2, 0, 0, 1, 1, 0, 0, 2],
+ [EDGES.FR, EDGES.UF, EDGES.UL, EDGES.UB, EDGES.BR, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.DR, EDGES.FL, EDGES.BL, EDGES.UR],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+)
+MOVES['F'] = new DeepCube(
+ [CORNERS.UFL, CORNERS.DLF, CORNERS.ULB, CORNERS.UBR, CORNERS.URF, CORNERS.DFR, CORNERS.DBL, CORNERS.DRB],
+ [1, 2, 0, 0, 2, 1, 0, 0],
+ [EDGES.UR, EDGES.FL, EDGES.UL, EDGES.UB, EDGES.DR, EDGES.FR, EDGES.DL, EDGES.DB, EDGES.UF, EDGES.DF, EDGES.BL, EDGES.BR],
+ [0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0]
+)
+MOVES['D'] = new DeepCube(
+ [CORNERS.URF, CORNERS.UFL, CORNERS.ULB, CORNERS.UBR, CORNERS.DLF, CORNERS.DBL, CORNERS.DRB, CORNERS.DFR],
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [EDGES.UR, EDGES.UF, EDGES.UL, EDGES.UB, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.DR, EDGES.FR, EDGES.FL, EDGES.BL, EDGES.BR],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+)
+MOVES['L'] = new DeepCube(
+ [CORNERS.URF, CORNERS.ULB, CORNERS.DBL, CORNERS.UBR, CORNERS.DFR, CORNERS.UFL, CORNERS.DLF, CORNERS.DRB],
+ [0, 1, 2, 0, 0, 2, 1, 0],
+ [EDGES.UR, EDGES.UF, EDGES.BL, EDGES.UB, EDGES.DR, EDGES.DF, EDGES.FL, EDGES.DB, EDGES.FR, EDGES.UL, EDGES.DL, EDGES.BR],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+)
+MOVES['B'] = new DeepCube(
+ [CORNERS.URF, CORNERS.UFL, CORNERS.UBR, CORNERS.DRB, CORNERS.DFR, CORNERS.DLF, CORNERS.ULB, CORNERS.DBL],
+ [0, 0, 1, 2, 0, 0, 2, 1],
+ [EDGES.UR, EDGES.UF, EDGES.UL, EDGES.BR, EDGES.DR, EDGES.DF, EDGES.DL, EDGES.BL, EDGES.FR, EDGES.FL, EDGES.UB, EDGES.DB],
+ [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1]
+)
diff --git a/test/simulate_moves.js b/test/simulate_moves.js
index 8cf5182..0e02b35 100644
--- a/test/simulate_moves.js
+++ b/test/simulate_moves.js
@@ -1,15 +1,16 @@
-
-import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
+import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
// Helper to print face
const printFace = (matrix, name) => {
console.log(`--- ${name} ---`);
- matrix.forEach(row => console.log(row.map(c => c ? c[0].toUpperCase() : '-').join(' ')));
+ matrix.forEach((row) =>
+ console.log(row.map((c) => (c ? c[0].toUpperCase() : "-")).join(" ")),
+ );
};
// Helper to check if a face matches expected color (center color)
const checkFaceColor = (matrix, expectedColor) => {
- return matrix.every(row => row.every(c => c === expectedColor));
+ return matrix.every((row) => row.every((c) => c === expectedColor));
};
console.log("=== RUBIK'S CUBE SIMULATION & DIAGNOSTIC ===");
@@ -19,7 +20,7 @@ const cube = new Cube();
// 1. Initial State Check
console.log("\n1. Checking Initial State...");
let state = cube.getState();
-const isSolved =
+const isSolved =
checkFaceColor(state[FACES.UP], COLORS.WHITE) &&
checkFaceColor(state[FACES.DOWN], COLORS.YELLOW) &&
checkFaceColor(state[FACES.FRONT], COLORS.GREEN) &&
@@ -52,20 +53,28 @@ console.log("\n2. Simulating: Left Layer (x=-1) Rotation (L-like move)...");
// Try direction = 1
console.log("-> Applying rotateLayer('x', -1, 1)...");
-cube.rotateLayer('x', -1, 1);
+cube.rotateLayer("x", -1, 1);
state = cube.getState();
// Check result on Left Column of Front Face
// Front is Green. Top is White.
// If L (Drag Down): Front-Left-Col should be White.
-const frontLeftCol = [state[FACES.FRONT][0][0], state[FACES.FRONT][1][0], state[FACES.FRONT][2][0]];
+const frontLeftCol = [
+ state[FACES.FRONT][0][0],
+ state[FACES.FRONT][1][0],
+ state[FACES.FRONT][2][0],
+];
console.log("Front Left Column colors:", frontLeftCol);
-if (frontLeftCol.every(c => c === COLORS.WHITE)) {
- console.log("✅ Result: Front got White (Top). This matches 'Drag Down' (L move).");
+if (frontLeftCol.every((c) => c === COLORS.WHITE)) {
+ console.log(
+ "✅ Result: Front got White (Top). This matches 'Drag Down' (L move).",
+ );
console.log("=> CONCLUSION: direction=1 corresponds to Drag Down (L).");
-} else if (frontLeftCol.every(c => c === COLORS.YELLOW)) {
- console.log("⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).");
+} else if (frontLeftCol.every((c) => c === COLORS.YELLOW)) {
+ console.log(
+ "⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).",
+ );
console.log("=> CONCLUSION: direction=1 corresponds to Drag Up (L').");
} else {
console.error("❌ Unexpected colors:", frontLeftCol);
@@ -89,7 +98,7 @@ cube.reset();
console.log("\n3. Simulating: Top Layer (y=1) Rotation...");
// Try direction = 1
console.log("-> Applying rotateLayer('y', 1, 1)...");
-cube.rotateLayer('y', 1, 1);
+cube.rotateLayer("y", 1, 1);
state = cube.getState();
// Check result on Top Row of Front Face
@@ -104,10 +113,10 @@ state = cube.getState();
const frontTopRow = state[FACES.FRONT][0];
console.log("Front Top Row colors:", frontTopRow);
-if (frontTopRow.every(c => c === COLORS.ORANGE)) {
+if (frontTopRow.every((c) => c === COLORS.ORANGE)) {
console.log("✅ Result: Front got Orange (Left). This matches 'Drag Right'.");
console.log("=> CONCLUSION: direction=1 corresponds to Drag Right.");
-} else if (frontTopRow.every(c => c === COLORS.RED)) {
+} else if (frontTopRow.every((c) => c === COLORS.RED)) {
console.log("⚠️ Result: Front got Red (Right). This matches 'Drag Left'.");
console.log("=> CONCLUSION: direction=1 corresponds to Drag Left.");
} else {
diff --git a/test/test_new_model.js b/test/test_new_model.js
index 34d6aa0..83736dc 100644
--- a/test/test_new_model.js
+++ b/test/test_new_model.js
@@ -1,7 +1,6 @@
+import { CubeModel, FACES, COLORS } from "../src/utils/CubeModel.js";
-import { CubeModel, FACES, COLORS } from '../src/utils/CubeModel.js';
-
-console.log('Running CubeModel Rotation Logic Tests...');
+console.log("Running CubeModel Rotation Logic Tests...");
const cube1 = new CubeModel();
const cube2 = new CubeModel();
@@ -14,9 +13,9 @@ const compareCubes = (c1, c2, message) => {
return true;
} else {
console.error(`❌ FAIL: ${message}`);
- console.log('Expected (Standard Move):');
+ console.log("Expected (Standard Move):");
console.log(s2);
- console.log('Actual (Layer Rotation):');
+ console.log("Actual (Layer Rotation):");
console.log(s1);
return false;
}
@@ -25,48 +24,47 @@ const compareCubes = (c1, c2, message) => {
// Test 1: Top Layer (y=1) CW vs U
cube1.reset();
cube2.reset();
-console.log('Testing Top Layer CW vs U...');
-cube1.rotateLayer('y', 1, 1); // Top CW
-cube2.applyMove('U');
+console.log("Testing Top Layer CW vs U...");
+cube1.rotateLayer("y", 1, 1); // Top CW
+cube2.applyMove("U");
compareCubes(cube1, cube2, "Top Layer CW matches U");
// Test 2: Bottom Layer (y=-1) CW vs D
cube1.reset();
cube2.reset();
-console.log('Testing Bottom Layer CW vs D...');
-cube1.rotateLayer('y', -1, -1); // Bottom CW (CW around -Y is CCW around Y)
-cube2.applyMove('D');
+console.log("Testing Bottom Layer CW vs D...");
+cube1.rotateLayer("y", -1, -1); // Bottom CW (CW around -Y is CCW around Y)
+cube2.applyMove("D");
compareCubes(cube1, cube2, "Bottom Layer CW matches D");
// Test 3: Left Layer (x=-1) CW vs L
cube1.reset();
cube2.reset();
-console.log('Testing Left Layer CW vs L...');
-cube1.rotateLayer('x', -1, -1); // Left CW (CW around -X is CCW around X)
-cube2.applyMove('L');
+console.log("Testing Left Layer CW vs L...");
+cube1.rotateLayer("x", -1, -1); // Left CW (CW around -X is CCW around X)
+cube2.applyMove("L");
compareCubes(cube1, cube2, "Left Layer CW matches L");
// Test 4: Right Layer (x=1) CW vs R
cube1.reset();
cube2.reset();
-console.log('Testing Right Layer CW vs R...');
-cube1.rotateLayer('x', 1, 1); // Right CW
-cube2.applyMove('R');
+console.log("Testing Right Layer CW vs R...");
+cube1.rotateLayer("x", 1, 1); // Right CW
+cube2.applyMove("R");
compareCubes(cube1, cube2, "Right Layer CW matches R");
// Test 5: Front Layer (z=1) CW vs F
cube1.reset();
cube2.reset();
-console.log('Testing Front Layer CW vs F...');
-cube1.rotateLayer('z', 1, 1); // Front CW
-cube2.applyMove('F');
+console.log("Testing Front Layer CW vs F...");
+cube1.rotateLayer("z", 1, 1); // Front CW
+cube2.applyMove("F");
compareCubes(cube1, cube2, "Front Layer CW matches F");
// Test 6: Back Layer (z=-1) CW vs B
cube1.reset();
cube2.reset();
-console.log('Testing Back Layer CW vs B...');
-cube1.rotateLayer('z', -1, -1); // Back CW (CW around -Z is CCW around Z)
-cube2.applyMove('B');
+console.log("Testing Back Layer CW vs B...");
+cube1.rotateLayer("z", -1, -1); // Back CW (CW around -Z is CCW around Z)
+cube2.applyMove("B");
compareCubes(cube1, cube2, "Back Layer CW matches B");
-
diff --git a/test/test_rubiks_import.js b/test/test_rubiks_import.js
deleted file mode 100644
index 6c905f0..0000000
--- a/test/test_rubiks_import.js
+++ /dev/null
@@ -1,12 +0,0 @@
-
-import { State } from 'rubiks-js/src/state/index.js';
-
-console.log('State imported successfully');
-const state = new State(true);
-console.log('State instantiated');
-
-state.applyTurn('R');
-console.log('Applied turn R');
-
-const encoded = state.encode();
-console.log('Encoded state:', encoded);
diff --git a/test/verify_integrity.js b/test/verify_integrity.js
new file mode 100644
index 0000000..876534c
--- /dev/null
+++ b/test/verify_integrity.js
@@ -0,0 +1,41 @@
+import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
+
+function runStressTest(iterations) {
+ console.log(`Starting DeepCube Stress Test (${iterations} moves)...`);
+
+ let cube = new DeepCube(); // Solved
+
+ const moveNames = Object.keys(MOVES);
+ const startTime = Date.now();
+
+ for (let i = 1; i <= iterations; i++) {
+ const randomMove = moveNames[Math.floor(Math.random() * moveNames.length)];
+ cube = cube.multiply(MOVES[randomMove]);
+
+ if (!cube.isValid()) {
+ console.error(`\n❌ INVALID STATE DETECTED AT MOVE ${i}!`);
+ console.error(`Move applied: ${randomMove}`);
+ console.error(`CP:`, cube.cp);
+ console.error(`CO:`, cube.co);
+ console.error(`EP:`, cube.ep);
+ console.error(`EO:`, cube.eo);
+ process.exit(1);
+ }
+
+ if (i % 100000 === 0) {
+ process.stdout.write(
+ `\r✅ ${i} moves verified (${((i / iterations) * 100).toFixed(0)}%)`,
+ );
+ }
+ }
+
+ const duration = Date.now() - startTime;
+ console.log(
+ `\n🎉 Success! Mathematical integrity held over ${iterations} random moves.`,
+ );
+ console.log(
+ `⏱️ Time taken: ${duration} ms (${(iterations / (duration / 1000)).toFixed(0)} moves/sec)`,
+ );
+}
+
+runStressTest(1000000);
diff --git a/test/verify_solvers.js b/test/verify_solvers.js
new file mode 100644
index 0000000..0b04dd6
--- /dev/null
+++ b/test/verify_solvers.js
@@ -0,0 +1,75 @@
+import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
+import { KociembaSolver } from "../src/utils/solvers/KociembaSolver.js";
+
+function generateScramble(length = 20) {
+ const moveNames = Object.keys(MOVES);
+ const scramble = [];
+ for (let i = 0; i < length; i++) {
+ scramble.push(moveNames[Math.floor(Math.random() * moveNames.length)]);
+ }
+ return scramble;
+}
+
+function runSolverTests(iterations) {
+ console.log(`Starting KociembaSolver tests (${iterations} scrambles)...`);
+ let successCount = 0;
+ let totalMoves = 0;
+
+ for (let i = 0; i < iterations; i++) {
+ let cube = new DeepCube();
+ const scramble = generateScramble(30);
+ scramble.forEach((m) => {
+ cube = cube.multiply(MOVES[m]);
+ });
+
+ const solver = new KociembaSolver(cube);
+
+ try {
+ const solution = solver.solve();
+
+ // Apply solution to verify
+ let testCube = cube.clone();
+ solution.forEach((m) => {
+ if (!MOVES[m]) console.error("MISSING MOVE FROM SOLVER:", m);
+ testCube = testCube.multiply(MOVES[m]);
+ });
+
+ if (testCube.isValid() && isSolvedState(testCube)) {
+ successCount++;
+ totalMoves += solution.length;
+ if (i % 10 === 0) process.stdout.write(`\r✅ ${i} solves complete.`);
+ } else {
+ console.error(`\n❌ Solver failed validation on scramble ${i}!`);
+ console.error(`Scramble: ${scramble.join(" ")}`);
+ console.error(`Solution: ${solution.join(" ")}`);
+ console.error(`CP:`, testCube.cp);
+ console.error(`CO:`, testCube.co);
+ console.error(`EP:`, testCube.ep);
+ console.error(`EO:`, testCube.eo);
+ process.exit(1);
+ }
+ } catch (e) {
+ console.error(`\n❌ Solver threw error on scramble ${i}!`);
+ console.error(`Scramble: ${scramble.join(" ")}`);
+ console.error(e);
+ process.exit(1);
+ }
+ }
+
+ console.log(
+ `\n🎉 Success! KociembaSolver solved ${successCount}/${iterations} cubes optimally.`,
+ );
+ console.log(
+ `📊 Average shortest path: ${(totalMoves / iterations).toFixed(1)} moves.`,
+ );
+}
+
+function isSolvedState(state) {
+ for (let i = 0; i < 8; i++)
+ if (state.cp[i] !== i || state.co[i] !== 0) return false;
+ for (let i = 0; i < 12; i++)
+ if (state.ep[i] !== i || state.eo[i] !== 0) return false;
+ return true;
+}
+
+runSolverTests(100);