rubiks-cube-solver
Version:
Outputs a solution using the Fridrich Method for a given cube state.
423 lines (361 loc) • 10.6 kB
JavaScript
import { Cubie } from './Cubie';
import { algorithmShortener } from '../algorithm-shortener';
import {
transformNotations, getMiddleMatchingFace
} from '../utils';
const SOLVED_STATE = 'fffffffffrrrrrrrrruuuuuuuuudddddddddlllllllllbbbbbbbbb';
class RubiksCube {
/**
* Factory method. Returns an instance of a solved Rubiks Cube.
*/
static Solved() {
return new RubiksCube(SOLVED_STATE);
}
/**
* Factory method.
* @param {string|array} moves
*/
static FromMoves(moves) {
const cube = RubiksCube.Solved();
cube.move(moves);
return cube;
}
/**
* Factory method. Returns an instance of a scrambled Rubiks Cube.
*/
static Scrambled() {
let cube = RubiksCube.Solved();
let randomMoves = RubiksCube.getRandomMoves(25);
cube.move(randomMoves);
return cube;
}
/**
* @param {string|array} notations - The list of moves to reverse.
* @return {string|array} -- whichever was initially given.
*/
static reverseMoves(moves) {
return RubiksCube.transformMoves(moves, { reverse: true });
}
/**
* @param {string|array} moves - The moves to transform;
* @param {object} options
* @prop {boolean} options.upperCase - Turn lowercase moves into uppercase.
* @prop {object} options.orientation - An object describing the orientation
* from which to makes the moves. See src/js/utils#orientMoves.
*
* @return {string|array} -- whichever was initially given.
*/
static transformMoves(moves, options = {}) {
return transformNotations(moves, options);
}
static getRandomMoves(length = 25) {
let randomMoves = [];
let totalMoves = [
'F',
'Fprime',
'R',
'Rprime',
'U',
'Uprime',
'D',
'Dprime',
'L',
'Lprime',
'B',
'Bprime'
];
while (randomMoves.length < length) {
for (let i = 0; i < length - randomMoves.length; i++) {
let idx = ~~(Math.random() * totalMoves.length);
randomMoves.push(totalMoves[idx]);
}
randomMoves = algorithmShortener(randomMoves).split(' ');
}
return randomMoves.join(' ');
}
/**
* @param {string} cubeState - The string representing the Rubik's Cube.
*
* The cube state are represented as:
* 'FFFFFFFFFRRRRRRRRRUUUUUUUUUDDDDDDDDDLLLLLLLLLBBBBBBBBB'
*
* where:
* F stands for the FRONT COLOR
* R stands for the RIGHT COLOR
* U stands for the UP COLOR
* D stands for the DOWN COLOR
* L stands for the LEFT COLOR
* B stands for the BACK COLOR
*
* and the faces are given in the order of:
* FRONT, RIGHT, UP, DOWN, LEFT, BACK
*
* The order of each color per face is ordered by starting from the top left
* corner and moving to the bottom right, as if reading lines of text.
*
* See this example: http://2.bp.blogspot.com/_XQ7FznWBAYE/S9Sbric1KNI/AAAAAAAAAFs/wGAb_LcSOwo/s1600/rubik.png
*/
constructor(cubeState) {
if (cubeState.length !== 9 * 6) {
throw new Error('Wrong number of colors provided');
}
this._notationToRotation = {
f: { axis: 'z', mag: -1 },
r: { axis: 'x', mag: -1 },
u: { axis: 'y', mag: -1 },
d: { axis: 'y', mag: 1 },
l: { axis: 'x', mag: 1 },
b: { axis: 'z', mag: 1 },
m: { axis: 'x', mag: 1 },
e: { axis: 'y', mag: 1 },
s: { axis: 'z', mag: -1 }
};
this._build(cubeState);
}
/**
* Grab all the cubes on a given face, and return them in order from top left
* to bottom right.
* @param {string} face - The face to grab.
* @return {array}
*/
getFace(face) {
if (typeof face !== 'string') {
throw new Error(`"face" must be a string (received: ${face})`);
}
face = face.toLowerCase()[0];
// The 3D position of cubies and the way they're ordered on each face
// do not play nicely. Below is a shitty way to reconcile the two.
// The way the cubies are sorted depends on the row and column they
// occupy on their face. Cubies on a higher row will have a lower sorting
// index, but rows are not always denoted by cubies' y position, and
// "higher rows" do not always mean "higher axis values".
let row, col, rowOrder, colOrder;
let cubies;
// grab correct cubies
if (face === 'f') {
[row, col, rowOrder, colOrder] = ['Y', 'X', -1, 1];
cubies = this._cubies.filter(cubie => cubie.getZ() === 1);
} else if (face === 'r') {
[row, col, rowOrder, colOrder] = ['Y', 'Z', -1, -1];
cubies = this._cubies.filter(cubie => cubie.getX() === 1);
} else if (face === 'u') {
[row, col, rowOrder, colOrder] = ['Z', 'X', 1, 1];
cubies = this._cubies.filter(cubie => cubie.getY() === 1);
} else if (face === 'd') {
[row, col, rowOrder, colOrder] = ['Z', 'X', -1, 1];
cubies = this._cubies.filter(cubie => cubie.getY() === -1);
} else if (face === 'l') {
[row, col, rowOrder, colOrder] = ['Y', 'Z', -1, 1];
cubies = this._cubies.filter(cubie => cubie.getX() === -1);
} else if (face === 'b') {
[row, col, rowOrder, colOrder] = ['Y', 'X', -1, -1];
cubies = this._cubies.filter(cubie => cubie.getZ() === -1);
} else if (['m', 'e', 's'].includes(face)) {
return this._getMiddleCubiesForMove(face);
}
// order cubies from top left to bottom right
return cubies.sort((first, second) => {
let firstCubieRow = first[`get${row}`]() * rowOrder;
let firstCubieCol = first[`get${col}`]() * colOrder;
let secondCubieRow = second[`get${row}`]() * rowOrder;
let secondCubieCol = second[`get${col}`]() * colOrder;
if (firstCubieRow < secondCubieRow) {
return -1;
} else if (firstCubieRow > secondCubieRow) {
return 1;
} else {
return firstCubieCol < secondCubieCol ? -1 : 1;
}
});
}
/**
* @param {array} faces - The list of faces the cubie belongs on.
*/
getCubie(faces) {
return this._cubies.find(cubie => {
if (faces.length != cubie.faces().length) {
return false;
}
for (let face of faces) {
if (!cubie.faces().includes(face)) {
return false;
}
}
return true;
});
}
/**
* Finds and returns all cubies with three colors.
* @return {array}
*/
corners() {
return this._cubies.filter(cubie => cubie.isCorner());
}
/**
* Finds and returns all cubies with two colors.
* @return {array}
*/
edges() {
return this._cubies.filter(cubie => cubie.isEdge());
}
/**
* Finds and returns all cubies with one color.
* @return {array}
*/
middles() {
return this._cubies.filter(cubie => cubie.isMiddle());
}
/**
* Gets the rotation axis and magnitude of rotation based on notation.
* Then finds all cubes on the correct face, and rotates them around the
* rotation axis.
* @param {string|array} notations - The move notation.
* @param {object} options - Move options.
* @prop {boolean} options.upperCase - Turn all moves to upper case (i.e. no "double" moves).
*/
move(notations, options = {}) {
if (typeof notations === 'string') {
notations = notations.split(' ');
}
notations = transformNotations(notations, options);
for (let notation of notations) {
let move = notation[0];
if (!move) {
continue;
}
let isPrime = notation.toLowerCase().includes('prime');
let isWithMiddle = move === move.toLowerCase();
let isDoubleMove = notation.includes('2');
let { axis, mag } = this._getRotationForFace(move);
let cubesToRotate = this.getFace(move);
if (isPrime) mag *= -1;
if (isDoubleMove) mag *= 2;
if (isWithMiddle) {
let middleMove = getMiddleMatchingFace(move);
let middleCubies = this._getMiddleCubiesForMove(middleMove);
cubesToRotate = [...cubesToRotate, ...middleCubies];
}
for (let cubie of cubesToRotate) {
cubie.rotate(axis, mag);
}
}
}
isSolved() {
return this.toString() === SOLVED_STATE;
}
toString() {
let cubeState = '';
let faces = ['front', 'right', 'up', 'down', 'left', 'back'];
for (let face of faces) {
let cubies = this.getFace(face);
for (let cubie of cubies) {
cubeState += cubie.getColorOfFace(face);
}
}
return cubeState;
}
clone() {
return new RubiksCube(this.toString());
}
/**
* Create a "virtual" cube, with individual "cubies" having a 3D coordinate
* position and 1 or more colors attached to them.
*/
_build(cubeState) {
this._cubies = [];
this._populateCube();
let parsedColors = this._parseColors(cubeState);
for (let face of Object.keys(parsedColors)) {
let colors = parsedColors[face];
this._colorFace(face, colors);
}
}
/**
* Populates the "virtual" cube with 26 "empty" cubies by their position.
* @return {null}
*/
_populateCube() {
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
// no cubie in the center of the rubik's cube
if (x === 0 && y === 0 && z === 0) {
continue;
}
let cubie = new Cubie({ position: [x, y, z] });
this._cubies.push(cubie);
}
}
}
}
/**
* @return {object} - A map with faces for keys and colors for values
*/
_parseColors(cubeState) {
let faceColors = {
front: [],
right: [],
up: [],
down: [],
left: [],
back: []
};
let currentFace;
for (let i = 0; i < cubeState.length; i++) {
let color = cubeState[i];
if (i < 9) {
currentFace = 'front';
} else if (i < 9 * 2) {
currentFace = 'right';
} else if (i < 9 * 3) {
currentFace = 'up';
} else if (i < 9 * 4) {
currentFace = 'down';
} else if (i < 9 * 5) {
currentFace = 'left';
} else {
currentFace = 'back';
}
faceColors[currentFace].push(color);
}
return faceColors;
}
/**
* @param {array} face - An array of the cubies on the given face.
* @param {array} colors - An array of the colors on the given face.
*/
_colorFace(face, colors) {
let cubiesToColor = this.getFace(face);
for (let i = 0; i < colors.length; i++) {
cubiesToColor[i].colorFace(face, colors[i]);
}
}
/**
* @return {object} - The the rotation axis and magnitude for the given face.
*/
_getRotationForFace(face) {
if (typeof face !== 'string') {
throw new Error(`"face" must be a string (received: ${face})`);
}
face = face.toLowerCase();
return {
axis: this._notationToRotation[face].axis,
mag: this._notationToRotation[face].mag * Math.PI / 2
};
}
_getMiddleCubiesForMove(move) {
move = move[0].toLowerCase();
let nonMiddles;
if (move === 'm') {
nonMiddles = ['left', 'right'];
} else if (move === 'e') {
nonMiddles = ['up', 'down'];
} else if (move === 's') {
nonMiddles = ['front', 'back'];
}
return this._cubies.filter(cubie => {
return !cubie.hasFace(nonMiddles[0]) && !cubie.hasFace(nonMiddles[1]);
});
}
}
export { RubiksCube };