UNPKG

rubiks-cube-solver

Version:

Outputs a solution using the Fridrich Method for a given cube state.

416 lines (346 loc) 12.3 kB
import cross from 'gl-vec3/cross'; import { Face } from '../models/Face'; import { Vector } from '../models/Vector'; // maps each face with the notation for their middle moves const _middlesMatchingFace = { f: 's', r: 'mprime', u: 'eprime', d: 'e', l: 'm', b: 'sprime' }; /** * @param {string} move - The notation of a move, e.g. rPrime. * @return {string} */ export const getFaceOfMove = (move) => { if (typeof move !== 'string') { throw new TypeError('move must be a string'); } let faceLetter = move[0].toLowerCase(); if (faceLetter === 'f') return 'front'; if (faceLetter === 'r') return 'right'; if (faceLetter === 'u') return 'up'; if (faceLetter === 'd') return 'down'; if (faceLetter === 'l') return 'left'; if (faceLetter === 'b') return 'back'; }; /** * Almost useless. Almost. * @param {string} face - The string identifying a face. * @return {string} */ export const getMoveOfFace = (face) => { if (typeof face !== 'string') { throw new TypeError('face must be a string'); } face = face.toLowerCase(); if (!['front', 'right', 'up', 'down', 'left', 'back'].includes(face)) { throw new Error(`${face} is not valid face`); } return face[0]; }; export const getMiddleMatchingFace = (face) => { face = face.toLowerCase()[0]; return _middlesMatchingFace[face]; }; export const getFaceMatchingMiddle = (middle) => { middle = middle.toLowerCase(); for (let face of Object.keys(_middlesMatchingFace)) { let testMiddle = _middlesMatchingFace[face]; if (middle === testMiddle) { return face; } } }; /** * @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). * * @return {string|array} -- whichever was initially given. */ export const transformNotations = (notations, options = {}) => { let normalized = normalizeNotations(notations); if (options.upperCase) { normalized = normalized.map(n => n[0].toUpperCase() + n.slice(1)); } if (options.orientation) { normalized = orientMoves(normalized, options.orientation); } if (options.reverse) { normalized = _reverseNotations(normalized); } return typeof notations === 'string' ? normalized.join(' ') : normalized; }; /** * @param {array|string} notations - The notations to noramlize. * @return {array} */ export const normalizeNotations = (notations) => { if (typeof notations === 'string') { notations = notations.split(' '); } notations = notations.filter(notation => notation !== ''); return notations.map(notation => { let isPrime = notation.toLowerCase().includes('prime'); let isDouble = notation.includes('2'); notation = notation[0]; if (isDouble) notation = notation[0] + '2'; else if (isPrime) notation = notation + 'prime'; return notation; }); }; /** * Finds the direction from an origin face to a target face. The origin face * will be oriented so that it becomes FRONT. An orientation object must be * provided that specifies any of these faces (exclusively): TOP, RIGHT, DOWN, * LEFT. * If FRONT or BACK is provided along with one of those faces, it will be * ignored. If FRONT or BACK is the only face provided, the orientation is * ambiguous and an error will be thrown. * * Example: * getDirectionFromFaces('back', 'up', { down: 'right' }) * Step 1) orient the BACK face so that it becomes FRONT. * Step 2) orient the DOWN face so that it becomes RIGHT. * Step 3) Find the direction from BACK (now FRONT) to UP (now LEFT). * Step 4) Returns 'left'. * * @param {string} origin - The origin face. * @param {string} target - The target face. * @param {object} orientation - The object that specifies the cube orientation. * @return {string|number} */ export const getDirectionFromFaces = (origin, target, orientation) => { orientation = _toLowerCase(orientation); orientation = _prepOrientationForDirection(orientation, origin); let fromFace = new Face(origin); let toFace = new Face(target); let rotations = _getRotationsForOrientation(orientation); _rotateFacesByRotations([fromFace, toFace], rotations); let axis = new Vector(cross([], fromFace.normal(), toFace.normal())).getAxis(); let direction = Vector.getAngle(fromFace.normal(), toFace.normal()); if (axis === 'x' && direction > 0) return 'down'; if (axis === 'x' && direction < 0) return 'up'; if (axis === 'y' && direction > 0) return 'right'; if (axis === 'y' && direction < 0) return 'left'; if (direction === 0) { return 'front'; } else if (direction === Math.PI) { return 'back'; } }; /** * See `getDirectionFromFaces`. Almost identical, but instead of finding a * direction from an origin face and target face, this finds a target face from * an origin face and direction. * @param {string} origin - The origin face. * @param {string} direction - The direction. * @param {object} orientation - The orientation object. * @return {string} */ export const getFaceFromDirection = (origin, direction, orientation) => { orientation = _toLowerCase(orientation); orientation = _prepOrientationForDirection(orientation, origin); let fromFace = new Face(origin); let rotations = _getRotationsForOrientation(orientation); _rotateFacesByRotations([fromFace], rotations); let directionFace = new Face(direction); let { axis, angle } = Vector.getRotationFromNormals(fromFace.normal(), directionFace.normal()); fromFace.rotate(axis, angle); // at this point fromFace is now the target face, but we still need to revert // the orientation to return the correct string let reversedRotations = rotations.map(rotation => Vector.reverseRotation(rotation)).reverse(); _rotateFacesByRotations([fromFace], reversedRotations); return fromFace.toString(); }; /** * Finds a move that rotates the given face around its normal, by the angle * described by normal1 -> normal2. * @param {string} face - The face to rotate. * @param {string} from - The origin face. * @param {string} to - The target face. * @return {string} */ export const getRotationFromTo = (face, from, to) => { const rotationFace = new Face(face); const fromFace = new Face(from); const toFace = new Face(to); let rotationAxis = rotationFace.vector.getAxis(); let [fromAxis, toAxis] = [fromFace.vector.getAxis(), toFace.vector.getAxis()]; if ([fromAxis.toLowerCase(), toAxis.toLowerCase()].includes(rotationAxis.toLowerCase())) { throw new Error(`moving ${rotationFace} from ${fromFace} to ${toFace} is not possible.`); } let move = getMoveOfFace(face).toUpperCase(); let angle = Vector.getAngle(fromFace.normal(), toFace.normal()); if (rotationFace.vector.getMagnitude() < 0) { angle *= -1; } if (angle === 0) { return ''; } else if (Math.abs(angle) === Math.PI) { return `${move} ${move}`; } else if (angle < 0) { return `${move}`; } else if (angle > 0) { return `${move}Prime`; } }; /** * Returns an array of transformed notations so that if done when the cube's * orientation is default (FRONT face is FRONT, RIGHT face is RIGHT, etc.), the * moves will have the same effect as performing the given notations on a cube * oriented by the specified orientation. * * Examples: * orientMoves(['R', 'U'], { front: 'front', up: 'up' }) === ['R', 'U'] * orientMoves(['R', 'U'], { front: 'front', down: 'right' }) === ['U', 'L'] * orientMoves(['R', 'U', 'LPrime', 'D'], { up: 'back', right: 'down' }) === ['D', 'B', 'UPrime', 'F'] * * @param {array} notations - An array of notation strings. * @param {object} orientation - The orientation object. */ export const orientMoves = (notations, orientation) => { orientation = _toLowerCase(orientation); let rotations = _getRotationsForOrientation(orientation); rotations.reverse().map(rotation => Vector.reverseRotation(rotation)); return notations.map(notation => { let isPrime = notation.toLowerCase().includes('prime'); let isDouble = notation.includes('2'); let isWithMiddle = notation[0] === notation[0].toLowerCase(); let isMiddle = ['m', 'e', 's'].includes(notation[0].toLowerCase()); if (isDouble) { notation = notation.replace('2', ''); } let face; if (isMiddle) { let faceStr = getFaceOfMove(getFaceMatchingMiddle(notation)); face = new Face(faceStr); } else { let faceStr = getFaceOfMove(notation[0]); face = new Face(faceStr); } _rotateFacesByRotations([face], rotations); let newNotation; // this will always be lower case if (isMiddle) { newNotation = getMiddleMatchingFace(face.toString()); } else { newNotation = face.toString()[0]; } if (!isWithMiddle) newNotation = newNotation.toUpperCase(); if (isDouble) newNotation = newNotation + '2'; if (isPrime && !isMiddle) newNotation += 'prime'; return newNotation; }); }; //----------------- // Helper functions //----------------- /** * Returns an object with all keys and values lowercased. Assumes all keys and * values are strings. * @param {object} object - The object to map. */ function _toLowerCase(object) { let ret = {}; Object.keys(object).forEach(key => { ret[key.toLowerCase()] = object[key].toLowerCase(); }); return ret; } /** * This function is specificly for `getDirectionFromFaces` and * `getFaceFromDirection`. It removes all keys that are either 'front' or 'back' * and sets the given front face to orientation.front. * @param {object} orientation - The orientation object. * @param {string} front - The face to set as front. */ function _prepOrientationForDirection(orientation, front) { let keys = Object.keys(orientation); if (keys.length <= 1 && ['front', 'back'].includes(keys[0])) { throw new Error(`Orientation object "${orientation}" is ambiguous. Please specify one of these faces: "up", "right", "down", "left"`); } // remove "front" and "back" from provided orientation object let temp = orientation; orientation = {}; keys.forEach(key => { if (['front', 'back'].includes(key)) { return; } orientation[key] = temp[key]; }); orientation.front = front.toLowerCase(); return orientation; } /** * @param {object} orientation - The orientation object. * @return {array} */ function _getRotationsForOrientation(orientation) { if (Object.keys(orientation) <= 1) { throw new Error(`Orientation object "${orientation}" is ambiguous. Please specify 2 faces.`); } let keys = Object.keys(orientation); let origins = keys.map(key => new Face(orientation[key])); let targets = keys.map(key => new Face(key)); // perform the first rotation, and save it let rotation1 = Vector.getRotationFromNormals( origins[0].normal(), origins[0].orientTo(targets[0]).normal() ); // perform the first rotation on the second origin face origins[1].rotate(rotation1.axis, rotation1.angle); // peform the second rotation, and save it let rotation2 = Vector.getRotationFromNormals( origins[1].normal(), origins[1].orientTo(targets[1]).normal() ); // if the rotation angle is PI, there are 3 possible axes that can perform the // rotation. however only one axis will perform the rotation while keeping // the first origin face on the target. this axis is the same as the origin // face's normal. if (Math.abs(rotation2.angle) === Math.PI) { let rotation2Axis = new Face(keys[0]).vector.getAxis(); rotation2.axis = rotation2Axis; } return [rotation1, rotation2]; } /** * @param {array} - Array of Face objects to rotate. * @param {array} - Array of rotations to apply to faces. * @return {null} */ function _rotateFacesByRotations(faces, rotations) { for (let face of faces) { for (let rotation of rotations) { face.rotate(rotation.axis, rotation.angle); } } } /** * @param {array} notations * @return {array} */ function _reverseNotations(notations) { const reversed = []; for (let notation of notations) { let isPrime = notation.includes('prime'); notation = isPrime ? notation[0] : notation[0] + 'prime'; reversed.push(notation); } return typeof moves === 'string' ? reversed.join(' ') : reversed; } export default { getFaceOfMove, getMoveOfFace, getMiddleMatchingFace, getFaceMatchingMiddle, transformNotations, normalizeNotations, getDirectionFromFaces, getRotationFromTo, getFaceFromDirection, orientMoves };