feat: reposition solver controls to a dropdown

Moved the Kociemba/Beginner solve options into a sleek dropdown menu positioned above the Scramble button on the left side of the screen. This ensures the solver controls no longer obstruct the programmatic move queue at the bottom.
This commit is contained in:
2026-02-23 21:46:15 +00:00
parent f6b34449df
commit 929761ac9e
31 changed files with 6843 additions and 987 deletions

View File

@@ -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");

View File

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

View File

@@ -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");

24
test/debug_kociemba.js Normal file
View File

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

197
test/generate_math.js Normal file
View File

@@ -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(", ")}]
)`);
});

36
test/math_output.txt Normal file
View File

@@ -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]
)

View File

@@ -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 {

View File

@@ -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");

View File

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

41
test/verify_integrity.js Normal file
View File

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

75
test/verify_solvers.js Normal file
View File

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