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.
399 lines
12 KiB
JavaScript
399 lines
12 KiB
JavaScript
/**
|
|
* Dedicated 3x3x3 Rubik's Cube Model
|
|
*
|
|
* Representation:
|
|
* A collection of 27 Cubie objects, each with position (x, y, z) and face colors.
|
|
* Coordinate System:
|
|
* x: Left (-1) to Right (1)
|
|
* y: Bottom (-1) to Top (1)
|
|
* z: Back (-1) to Front (1)
|
|
*
|
|
* This logical model maintains the state of the cube and handles rotations.
|
|
*/
|
|
|
|
export const COLORS = {
|
|
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",
|
|
};
|
|
|
|
// Standard Face Colors (Solved State)
|
|
const SOLVED_COLORS = {
|
|
[FACES.UP]: COLORS.WHITE,
|
|
[FACES.DOWN]: COLORS.YELLOW,
|
|
[FACES.LEFT]: COLORS.ORANGE,
|
|
[FACES.RIGHT]: COLORS.RED,
|
|
[FACES.FRONT]: COLORS.GREEN,
|
|
[FACES.BACK]: COLORS.BLUE,
|
|
};
|
|
|
|
class Cubie {
|
|
constructor(id, x, y, z) {
|
|
this.id = id;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.faces = {
|
|
[FACES.UP]: COLORS.BLACK,
|
|
[FACES.DOWN]: COLORS.BLACK,
|
|
[FACES.LEFT]: COLORS.BLACK,
|
|
[FACES.RIGHT]: COLORS.BLACK,
|
|
[FACES.FRONT]: COLORS.BLACK,
|
|
[FACES.BACK]: COLORS.BLACK,
|
|
};
|
|
this.initColors();
|
|
}
|
|
|
|
// Set initial colors based on position in solved state
|
|
initColors() {
|
|
if (this.y === 1) this.faces[FACES.UP] = SOLVED_COLORS[FACES.UP];
|
|
if (this.y === -1) this.faces[FACES.DOWN] = SOLVED_COLORS[FACES.DOWN];
|
|
if (this.x === -1) this.faces[FACES.LEFT] = SOLVED_COLORS[FACES.LEFT];
|
|
if (this.x === 1) this.faces[FACES.RIGHT] = SOLVED_COLORS[FACES.RIGHT];
|
|
if (this.z === 1) this.faces[FACES.FRONT] = SOLVED_COLORS[FACES.FRONT];
|
|
if (this.z === -1) this.faces[FACES.BACK] = SOLVED_COLORS[FACES.BACK];
|
|
}
|
|
}
|
|
|
|
export class CubeModel {
|
|
constructor() {
|
|
this.size = 3;
|
|
this.cubies = [];
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.cubies = [];
|
|
let id = 0;
|
|
for (let x = -1; x <= 1; x++) {
|
|
for (let y = -1; y <= 1; y++) {
|
|
for (let z = -1; z <= 1; z++) {
|
|
this.cubies.push(new Cubie(id++, x, y, z));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Rotates a layer around an axis.
|
|
* @param {string} axis - 'x', 'y', 'z'
|
|
* @param {number} index - -1 (Left/Bottom/Back), 0 (Middle), 1 (Right/Top/Front)
|
|
* @param {number} direction - 1 (CW), -1 (CCW) relative to axis positive direction
|
|
*/
|
|
rotateLayer(axis, index, direction) {
|
|
// Determine the relevant cubies in the slice
|
|
const slice = this.cubies.filter((c) => c[axis] === index);
|
|
|
|
// Coordinate rotation (Matrix Logic)
|
|
// 90 deg CW rotation formulas:
|
|
// X-Axis: (y, z) -> (-z, y)
|
|
// Y-Axis: (x, z) -> (z, -x)
|
|
// Z-Axis: (x, y) -> (-y, x)
|
|
// Note: direction 1 is usually CCW in math (right hand rule around axis).
|
|
// Let's verify standard:
|
|
// Right Hand Rule with Thumb along Axis: Fingers curl in Positive Rotation direction.
|
|
// X (Right): Curl from Y (Up) to Z (Front). (0,1,0)->(0,0,1).
|
|
// y' = -z, z' = y?
|
|
// Let's check (0,1): y=1, z=0 -> y'=0, z'=1. Correct.
|
|
|
|
// So if direction is 1 (Positive/CW around axis):
|
|
// X: y' = -z, z' = y
|
|
// Y: z' = -x, x' = z
|
|
// Z: x' = -y, y' = x
|
|
|
|
// If direction is -1: Inverse.
|
|
|
|
slice.forEach((cubie) => {
|
|
this._rotateCubieCoordinates(cubie, axis, direction);
|
|
this._rotateCubieFaces(cubie, axis, direction);
|
|
});
|
|
}
|
|
|
|
_rotateCubieCoordinates(cubie, axis, direction) {
|
|
const { x, y, z } = cubie;
|
|
|
|
if (axis === "x") {
|
|
if (direction === 1) {
|
|
cubie.y = -z;
|
|
cubie.z = y;
|
|
} else {
|
|
cubie.y = z;
|
|
cubie.z = -y;
|
|
}
|
|
} else if (axis === "y") {
|
|
if (direction === 1) {
|
|
cubie.z = -x;
|
|
cubie.x = z;
|
|
} else {
|
|
cubie.z = x;
|
|
cubie.x = -z;
|
|
}
|
|
} else if (axis === "z") {
|
|
if (direction === 1) {
|
|
// CW
|
|
cubie.x = -y;
|
|
cubie.y = x;
|
|
} else {
|
|
// CCW
|
|
cubie.x = y;
|
|
cubie.y = -x;
|
|
}
|
|
}
|
|
}
|
|
|
|
_rotateCubieFaces(cubie, axis, direction) {
|
|
// When a cubie rotates, its faces move to new positions.
|
|
// We swap the COLORS on the faces.
|
|
// Example: Rotate X (Roll Forward). Up Face becomes Front Face.
|
|
// So new Front Color = old Up Color.
|
|
// cubie.faces[FRONT] = old_faces[UP]
|
|
|
|
const f = { ...cubie.faces };
|
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
cubie.faces[FACES.RIGHT] = f[FACES.UP];
|
|
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
|
|
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
|
|
cubie.faces[FACES.UP] = f[FACES.LEFT];
|
|
}
|
|
}
|
|
}
|
|
|
|
toCubies() {
|
|
// Return copy of state for rendering
|
|
// CubeCSS expects array of objects with x, y, z, faces
|
|
return this.cubies.map((c) => ({
|
|
id: c.id,
|
|
x: c.x,
|
|
y: c.y,
|
|
z: c.z,
|
|
faces: { ...c.faces },
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Applies a standard Rubik's Cube move
|
|
* @param {string} move - e.g. "U", "R'", "F2"
|
|
*/
|
|
applyMove(move) {
|
|
const base = move[0];
|
|
const modifier = move.substring(1);
|
|
let direction = -1; // Standard CW is -1 for U, L, F, B? Let's check.
|
|
// Direction Mapping based on rotateLayer Math:
|
|
// X(1) = CW (Up->Front)
|
|
// Y(1) = CW (Right->Front)
|
|
// Z(1) = CCW (Right->Up)
|
|
|
|
// R (CW around X): 1
|
|
// L (CW around -X): -1
|
|
// U (CW around Y): 1
|
|
// D (CW around -Y): -1
|
|
// F (CW around Z): -1 (since Z(1) is CCW)
|
|
// 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;
|
|
}
|
|
|
|
if (modifier === "'") direction *= -1;
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug printer for tests
|
|
toString() {
|
|
let out = "Cube State (3x3x3):\n";
|
|
// We can print faces.
|
|
// Order: U, D, F, B, L, R
|
|
const printFace = (face, name) => {
|
|
out += `Face ${name}:\n`;
|
|
// Grid 3x3.
|
|
// U: y=1. x from -1 to 1. z from -1 to 1.
|
|
// Coordinate mapping depends on face.
|
|
// Let's iterate standard grid rows/cols.
|
|
for (let r = 0; r < 3; r++) {
|
|
let rowStr = "";
|
|
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).
|
|
// 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.
|
|
// 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.
|
|
// 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,
|
|
);
|
|
|
|
if (cubie) {
|
|
rowStr += cubie.faces[face][0].toUpperCase() + " ";
|
|
} else {
|
|
rowStr += "? ";
|
|
}
|
|
}
|
|
out += rowStr + "\n";
|
|
}
|
|
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");
|
|
return out;
|
|
}
|
|
scramble(n = 20) {
|
|
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)
|
|
// D: y=-1, dir 1
|
|
// L: x=-1, dir -1
|
|
// R: x=1, dir 1
|
|
// F: z=1, dir 1
|
|
// B: z=-1, dir -1
|
|
|
|
// We can just generate random rotateLayer calls
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const axis = axes[Math.floor(Math.random() * axes.length)];
|
|
// Allow middle layer? Standard Scramble usually doesnt.
|
|
const index = indices[Math.floor(Math.random() * indices.length)];
|
|
const dir = Math.random() > 0.5 ? 1 : -1;
|
|
this.rotateLayer(axis, index, dir);
|
|
}
|
|
}
|
|
}
|