├── .gitignore ├── resources ├── banner.png └── animate-demo.gif ├── demo ├── package.json ├── app.js └── index.html ├── package.json ├── test ├── index.html └── test.js ├── src ├── index.js ├── renderer │ ├── shaders.js │ ├── render.js │ ├── buffer.js │ └── math.js └── model │ ├── consts.js │ ├── cube.js │ ├── solver.js │ └── rules.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /resources/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doodlewind/freecube/HEAD/resources/banner.png -------------------------------------------------------------------------------- /resources/animate-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doodlewind/freecube/HEAD/resources/animate-demo.gif -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freecube-demo", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freecube", 3 | "version": "0.1.1", 4 | "main": "src/index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Freecube Test 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Freecube.js for Jade Lin 3 | * (c) 2018 Yifeng Wang 4 | * Released under the MIT License. 5 | */ 6 | 7 | export { Cube } from './model/cube.js' 8 | export { Solver } from './model/solver.js' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yifeng Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | import { Cube } from '../src/model/cube.js' 3 | import { 4 | matchOrientationRule, 5 | matchPermutationRule, 6 | isOrientationSolved, 7 | isPermutationSolved 8 | } from '../src/model/solver/js' 9 | import { OLL, PLL } from '../src/model/rules.js' 10 | 11 | console.clear() 12 | 13 | const getTopColors = (cube) => { 14 | const result = [] 15 | ;[ 16 | [0, 1, 1], [1, 1, 0], [0, 1, -1], [-1, 1, 0], 17 | [1, 1, 1], [1, 1, -1], [-1, 1, -1], [-1, 1, 1] 18 | ].forEach(coord => result.push([...cube.getBlock(coord).colors])) 19 | return result 20 | } 21 | const flip = moves => moves.map(x => x.length > 1 ? x[0] : x + "'").reverse() 22 | 23 | OLL.forEach(rule => { 24 | const rMoves = flip(rule.moves) 25 | const cube = new Cube(null, rMoves) 26 | if ( 27 | matchOrientationRule(cube, rule) && 28 | isOrientationSolved(cube.move(rule.moves)) 29 | ) { 30 | console.log('OLL test pass', rule.id) 31 | } else console.error('Error OLL rule match', rule.id) 32 | }) 33 | 34 | PLL.forEach(rule => { 35 | const rMoves = flip(rule.moves) 36 | const cube = new Cube(null, rMoves) 37 | if (matchPermutationRule(cube, rule)) { 38 | const colors = getTopColors(cube.move(rule.moves)) 39 | if (isPermutationSolved(colors)) { 40 | console.log('PLL test pass', rule.name) 41 | } else console.error('Error PLL rule match', rule.name) 42 | } else console.error('Error PLL rule match', rule.name) 43 | }) 44 | -------------------------------------------------------------------------------- /src/renderer/shaders.js: -------------------------------------------------------------------------------- 1 | const vsSource = ` 2 | attribute vec4 aVertexPosition; 3 | attribute vec4 aVertexColor; 4 | 5 | uniform mat4 uModelViewMatrix; 6 | uniform mat4 uProjectionMatrix; 7 | 8 | varying lowp vec4 vColor; 9 | 10 | void main(void) { 11 | gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; 12 | vColor = aVertexColor; 13 | } 14 | ` 15 | 16 | const fsSource = ` 17 | varying lowp vec4 vColor; 18 | 19 | void main(void) { 20 | gl_FragColor = vColor; 21 | } 22 | ` 23 | 24 | function loadShader (gl, type, source) { 25 | const shader = gl.createShader(type) 26 | gl.shaderSource(shader, source) 27 | gl.compileShader(shader) 28 | 29 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 30 | console.log('Error compiling shaders', gl.getShaderInfoLog(shader)) 31 | gl.deleteShader(shader) 32 | return null 33 | } 34 | 35 | return shader 36 | } 37 | 38 | function initShaderProgram (gl, vsSource, fsSource) { 39 | const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource) 40 | const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource) 41 | 42 | const shaderProgram = gl.createProgram() 43 | gl.attachShader(shaderProgram, vertexShader) 44 | gl.attachShader(shaderProgram, fragmentShader) 45 | gl.linkProgram(shaderProgram) 46 | 47 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 48 | console.log( 49 | 'Error init shader program', gl.getProgramInfoLog(shaderProgram) 50 | ) 51 | return null 52 | } 53 | 54 | return shaderProgram 55 | } 56 | 57 | export function initProgram (gl) { 58 | const shaderProgram = initShaderProgram(gl, vsSource, fsSource) 59 | 60 | const programInfo = { 61 | program: shaderProgram, 62 | attribLocations: { 63 | vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), 64 | vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor') 65 | }, 66 | uniformLocations: { 67 | projectionMatrix: gl.getUniformLocation( 68 | shaderProgram, 'uProjectionMatrix' 69 | ), 70 | modelViewMatrix: gl.getUniformLocation( 71 | shaderProgram, 'uModelViewMatrix' 72 | ) 73 | } 74 | } 75 | return programInfo 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/render.js: -------------------------------------------------------------------------------- 1 | import { create, perspective, translate, rotate } from './math.js' 2 | 3 | export const getMats = (w, h, rX, rY) => { 4 | const fov = 30 * Math.PI / 180 5 | const [ 6 | aspect, zNear, zFar, projectionMat 7 | ] = [w / h, 0.1, 100.0, create()] 8 | perspective(projectionMat, fov, aspect, zNear, zFar) 9 | 10 | const modelViewMat = create() 11 | translate(modelViewMat, modelViewMat, [-0.0, 0.0, -25.0]) 12 | rotate(modelViewMat, modelViewMat, rX / 180 * Math.PI, [1, 0, 0]) 13 | rotate(modelViewMat, modelViewMat, rY / 180 * Math.PI, [0, 1, 0]) 14 | return { projectionMat, modelViewMat } 15 | } 16 | 17 | const draw = (gl, programInfo, mats, buffer) => { 18 | const { projectionMat, modelViewMat } = mats 19 | const { vertexPosition, vertexColor } = programInfo.attribLocations 20 | const { projectionMatrix, modelViewMatrix } = programInfo.uniformLocations 21 | 22 | { 23 | const [ 24 | numComponents, type, normalize, stride, offset 25 | ] = [3, gl.FLOAT, false, 0, 0] 26 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.positions) 27 | gl.vertexAttribPointer( 28 | vertexPosition, numComponents, type, normalize, stride, offset 29 | ) 30 | gl.enableVertexAttribArray(vertexPosition) 31 | } 32 | 33 | { 34 | const [ 35 | numComponents, type, normalize, stride, offset 36 | ] = [4, gl.FLOAT, false, 0, 0] 37 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.colors) 38 | gl.vertexAttribPointer( 39 | vertexColor, numComponents, type, normalize, stride, offset 40 | ) 41 | gl.enableVertexAttribArray(vertexColor) 42 | } 43 | 44 | gl.uniformMatrix4fv(projectionMatrix, false, projectionMat) 45 | gl.uniformMatrix4fv(modelViewMatrix, false, modelViewMat) 46 | 47 | { 48 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.indices) 49 | const [offset, type, vertexCount] = [0, gl.UNSIGNED_SHORT, 36 * 27] 50 | gl.drawElements(gl.TRIANGLES, vertexCount, type, offset) 51 | } 52 | } 53 | 54 | export const renderFrame = (gl, programInfo, buffer, rX, rY) => { 55 | const { clientWidth, clientHeight } = gl.canvas 56 | const mats = getMats(clientWidth, clientHeight, rX, rY) 57 | gl.clearColor(0.0, 0.0, 0.0, 0.0) 58 | gl.clearDepth(1.0) 59 | gl.enable(gl.DEPTH_TEST) 60 | gl.depthFunc(gl.LEQUAL) 61 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 62 | 63 | draw(gl, programInfo, mats, buffer) 64 | } 65 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | // Use `parcel index.html` for running this demo. 2 | 3 | /* eslint-env browser */ 4 | import { Cube } from '../src/model/cube.js' 5 | import { Solver } from '../src/model/solver.js' 6 | 7 | const $canvas = document.querySelector('#glcanvas') 8 | const cube = new Cube($canvas, JSON.parse(localStorage.moves || '[]')) 9 | const solver = new Solver(cube) 10 | 11 | const isMobile = window.orientation > -1 12 | const [E_START, E_MOVE, E_END] = isMobile 13 | ? ['touchstart', 'touchmove', 'touchend'] 14 | : ['mousedown', 'mousemove', 'mouseup'] 15 | 16 | let [rX, rY] = [30, -45] 17 | 18 | $canvas.addEventListener(E_START, e => { 19 | e.preventDefault() 20 | const _e = isMobile ? e.touches[0] : e 21 | const [baseX, baseY] = [_e.clientX, _e.clientY] 22 | const [_rX, _rY] = [rX, rY] 23 | 24 | const onMove = e => { 25 | const _e = isMobile ? e.touches[0] : e 26 | const [moveX, moveY] = [_e.clientX, _e.clientY] 27 | const baseSize = document.body.clientWidth / 2 28 | const percentX = (moveX - baseX) / baseSize 29 | const percentY = (moveY - baseY) / baseSize 30 | rX = _rX + 180 * percentY; rY = _rY + 180 * percentX 31 | cube.rX = rX; cube.rY = rY 32 | if (!cube.__ANIMATING) cube.render(rX, rY) 33 | } 34 | const onEnd = e => { 35 | document.removeEventListener(E_MOVE, onMove) 36 | document.removeEventListener(E_END, onEnd) 37 | } 38 | document.addEventListener(E_MOVE, onMove) 39 | document.addEventListener(E_END, onEnd) 40 | }) 41 | 42 | const flat = arr => { 43 | if (arr.includes(null)) return [] 44 | else if (arr.every(x => typeof x === 'string')) return arr 45 | return arr.reduce((a, b) => [...a, ...b], []) 46 | } 47 | 48 | const rules = [ 49 | { id: 'btn-shuffle', ops: () => cube.shuffle(20, true) }, 50 | { id: 'btn-cross', ops: () => cube.animate(flat(solver.solveCross())) }, 51 | { id: 'btn-f2l', ops: () => cube.animate(flat(solver.solveF2L() || [])) }, 52 | { id: 'btn-oll', ops: () => cube.animate(flat(solver.solveOLL() || [])) }, 53 | { id: 'btn-pll', ops: () => cube.animate(flat(solver.solvePLL() || [])) } 54 | ] 55 | rules.forEach(rule => { 56 | document.getElementById(rule.id).addEventListener('click', rule.ops) 57 | }) 58 | document.getElementById('help').addEventListener('click', () => { 59 | alert(`Shuffle: 随机打乱 60 | Cross: 还原底部十字 61 | F2L: 还原底部两层 62 | OLL: 还原顶面颜色 63 | PLL: 还原顶层顺序 64 | --------------- 65 | Enjoy it`) 66 | }) 67 | 68 | cube.rX = rX; cube.rY = rY; cube.render(rX, rY) 69 | window.cube = cube; window.solver = solver 70 | document.getElementById('share').src = $canvas.toDataURL() 71 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Freecube - 魔方复原模拟器 6 | 7 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/renderer/buffer.js: -------------------------------------------------------------------------------- 1 | import { rotateX, rotateY, rotateZ } from './math.js' 2 | 3 | const coordInFace = (coord, face) => { 4 | return { 5 | U: () => coord[1] === 1, 6 | D: () => coord[1] === -1, 7 | L: () => coord[0] === -1, 8 | R: () => coord[0] === 1, 9 | F: () => coord[2] === 1, 10 | B: () => coord[2] === -1 11 | }[face]() 12 | } 13 | 14 | const rotatorByFace = (face) => ({ 15 | U: rotateY, D: rotateY, L: rotateX, R: rotateX, F: rotateZ, B: rotateZ 16 | }[face]) 17 | 18 | const baseIndices = [ 19 | 0, 1, 2, 0, 2, 3, // front 20 | 4, 5, 6, 4, 6, 7, // back 21 | 8, 9, 10, 8, 10, 11, // top 22 | 12, 13, 14, 12, 14, 15, // bottom 23 | 16, 17, 18, 16, 18, 19, // right 24 | 20, 21, 22, 20, 22, 23 // left 25 | ] 26 | 27 | export function getBuffer (gl, blocks, moveFace, moveAngle) { 28 | let [positions, faceColors, colors, indices] = [[], [], [], []] 29 | 30 | for (let x = -1; x <= 1; x++) { 31 | for (let y = -1; y <= 1; y++) { 32 | for (let z = -1; z <= 1; z++) { 33 | const i = (x + 1) * 9 + (y + 1) * 3 + z + 1 34 | if (moveFace && coordInFace([x, y, z], moveFace)) { 35 | const pos = blocks[i].positions 36 | for (let j = 0; j < 72; j += 3) { 37 | const tmp = [] 38 | rotatorByFace(moveFace)( 39 | tmp, 40 | [pos[j], pos[j + 1], pos[j + 2]], 41 | [0, 0, 0], 42 | moveAngle / 180 * Math.PI 43 | ) 44 | positions.push(tmp[0], tmp[1], tmp[2]) 45 | } 46 | } else { 47 | positions = [...positions, ...blocks[i].positions] 48 | } 49 | } 50 | } 51 | } 52 | 53 | for (let i = 0; i < blocks.length; i++) { 54 | faceColors = [...faceColors, ...blocks[i].colors] 55 | indices = [...indices, ...baseIndices.map(x => x + i * 24)] 56 | } 57 | 58 | for (let i = 0; i < faceColors.length; i++) { 59 | const c = faceColors[i] 60 | // Repeat each color four times for the four vertices of the face. 61 | colors = colors.concat(c, c, c, c) 62 | } 63 | 64 | const positionBuffer = gl.createBuffer() 65 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer) 66 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW) 67 | 68 | const colorBuffer = gl.createBuffer() 69 | gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer) 70 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW) 71 | 72 | const indexBuffer = gl.createBuffer() 73 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer) 74 | gl.bufferData( 75 | gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW 76 | ) 77 | 78 | return { 79 | positions: positionBuffer, 80 | colors: colorBuffer, 81 | indices: indexBuffer 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Freecube 2 | ⚛ Solve Rubik's Cube with WebGL in 10KB. 3 | 4 | ![banner](./resources/banner.png) 5 | 6 | Freecube renders and animates Rubik's Cube with raw WebGL, plus a tiny rule-based solver showing how CFOP works. 7 | 8 | ![animate-demo](./resources/animate-demo.gif) 9 | 10 | 11 | ## Usage 12 | 13 | ``` 14 | npm install freecube 15 | ``` 16 | 17 | ``` js 18 | import { Cube, Solver } from 'freecube' 19 | 20 | const canvas = document.querySelector('#gl') 21 | const cube = new Cube(canvas) 22 | const solver = new Solver(cube) 23 | 24 | cube.render(30, -45) // Render the cube with X and Y rotation. 25 | cube.shuffle(20, true) // Shuffle it with animation. 26 | solver.solve() // Generate solution with CFOP algorithm. 27 | ``` 28 | 29 | 30 | ## API 31 | 32 | ### `Cube` 33 | `new Cube(canvas: CanvasElement, moves: Moves)` 34 | 35 | Main class emulating Rubik's Cube, it maintains cube state in `this.blocks` and optional WebGL instance in `this.gl`. 36 | 37 | * `canvas` - DOM canvas element for rendering, can be null for "headless" case. 38 | * `moves` - Array of cube moves, .e.g., `['R', 'U', 'F']`. 39 | 40 | #### `animate` 41 | `(move: String|Moves, duration: number) => Promise` 42 | 43 | Animate the cube moves with single or multi `move`, uses `cube.move` under the hood. you can set move speed with optional `durataion` args. The promise returned will be resolved on animation ends. 44 | 45 | #### `getBlock` 46 | `(coord: Coord) => Block` 47 | 48 | Get block data in the cube. `coord` shapes as `[0, 1, -1]` and the block returns in `{ positions: Array, colors: Array }` format. `block.positions` is the vertex positions of the block, and `block.colors` represents block colors in `[F, B, U, D, R, L]` sequence. 49 | 50 | #### `move` 51 | `(move: String|Moves) => Cube` 52 | 53 | Update cube state with moves passed in, uses `cube.rotate` under the hood. Just use it as `animate` without animation. Chain calling like `cube.move('U').move('R')` is supported. 54 | 55 | > Only `F / B / L / R / U / D` and their counter moves can be used for now. "Advanced" turns like `M / r / x / R2` are not supported. 56 | 57 | #### `rotate` 58 | `(center: Coord, clockwise: boolean) => Cube` 59 | 60 | Rotate cube face in 90 degree. `center` coord is the center block of the face, `clockwise` for rotate direction. 61 | 62 | #### `render` 63 | `(rX: number, rY: number, moveFace: String, moveAngle: number)` 64 | 65 | Renders the cube with `rX` and `rY` as overall rotation, and `moveFace` and `moveAngle` for a cube face rotation. 66 | 67 | #### `shuffle` 68 | `(n: number, animate: false) => Cube|Promise` 69 | 70 | Shuffles the cube. Returns cube instance if animation not used, and promise on shuffle animation ends. 71 | 72 | ### `Solver` 73 | Rule-based module solving Rubik's Cube with CFOP algorithm. 74 | 75 | #### `solve` 76 | `() => Array` 77 | 78 | Solve the cube state with all CFOP steps. Returns array of 4 series of moves. 79 | 80 | #### `solveCross` 81 | `() => Moves` 82 | 83 | Returns the moves to build a cross. 84 | 85 | #### `solveF2L` 86 | `() => Moves` 87 | 88 | Returns the moves to build first two layers. 89 | 90 | #### `solveOLL` 91 | `() => Moves` 92 | 93 | Returns the moves to pass OLL. 94 | 95 | #### `solvePLL` 96 | `() => Moves` 97 | 98 | Returns the moves to pass PLL. 99 | 100 | 101 | ## License 102 | MIT 103 | -------------------------------------------------------------------------------- /src/model/consts.js: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | WHITE: [0.9, 0.9, 0.9, 1.0], 3 | GREEN: [0.0, 1.0, 0.0, 1.0], 4 | RED: [1.0, 0.0, 0.0, 1.0], 5 | BLUE: [0.0, 0.0, 1.0, 1.0], 6 | ORANGE: [1.0, 0.5, 0.0, 1.0], 7 | YELLOW: [1.0, 1.0, 0.0, 1.0], 8 | EMPTY: [0.0, 0.0, 0.0, 0.0] 9 | } 10 | 11 | export const [ 12 | COLOR_U, COLOR_D, COLOR_F, COLOR_R 13 | ] = [COLORS.YELLOW, COLORS.WHITE, 0, 1] 14 | 15 | export const [F, B, U, D, R, L] = [0, 1, 2, 3, 4, 5] 16 | 17 | export const [S, E, N, W, SE, NE, NW, SW] = [0, 1, 2, 3, 4, 5, 6, 7] 18 | 19 | export const [SLOT_M, SLOT_D] = [8, 9] 20 | 21 | export const EDGE_GRIDS = [S, E, N, W] 22 | 23 | export const CORNER_GRIDS = [SW, NE, NW, SE] 24 | 25 | export const FACE_MAPPING = { 26 | '-1': { 27 | '-1': { '-1': [B, D, L], '0': [D, L], '1': [F, D, L] }, 28 | '0': { '-1': [B, L], '0': [L], '1': [F, L] }, 29 | '1': { '-1': [B, U, L], '0': [U, L], '1': [F, U, L] } 30 | }, 31 | '0': { 32 | '-1': { '-1': [B, D], '0': [D], '1': [F, D] }, 33 | '0': { '-1': [B], '0': [], '1': [F] }, 34 | '1': { '-1': [B, U], '0': [U], '1': [F, U] } 35 | }, 36 | '1': { 37 | '-1': { '-1': [B, D, R], '0': [D, R], '1': [F, D, R] }, 38 | '0': { '-1': [B, R], '0': [R], '1': [F, R] }, 39 | '1': { '-1': [B, U, R], '0': [U, R], '1': [F, U, R] } 40 | } 41 | } 42 | 43 | export const FACES = [ 44 | COLORS.BLUE, COLORS.GREEN, COLORS.YELLOW, 45 | COLORS.WHITE, COLORS.RED, COLORS.ORANGE 46 | ] 47 | 48 | export const EDGE_COORDS = [ 49 | // Bottom layer 50 | [0, -1, -1], [1, -1, 0], [0, -1, 1], [-1, -1, 0], 51 | // Second layer 52 | [-1, 0, 1], [1, 0, 1], [1, 0, -1], [-1, 0, -1], 53 | // Top layer 54 | [0, 1, 1], [1, 1, 0], [0, 1, -1], [-1, 1, 0] 55 | ] 56 | 57 | export const CORNER_COORDS = [ 58 | // Bottom layer 59 | [1, -1, 1], [1, -1, -1], [-1, -1, -1], [-1, -1, 1], 60 | // Top layer 61 | [1, 1, 1], [1, 1, -1], [-1, 1, -1], [-1, 1, 1] 62 | ] 63 | 64 | export const BLOCK_COORDS = [...EDGE_COORDS, ...CORNER_COORDS] 65 | 66 | export const PAIRS = [ 67 | [COLORS.BLUE, COLORS.RED], [COLORS.RED, COLORS.GREEN], 68 | [COLORS.GREEN, COLORS.ORANGE], [COLORS.ORANGE, COLORS.BLUE] 69 | ] 70 | 71 | export const SLOT_COORDS_MAPPING = { 72 | [PAIRS[0]]: [[1, -1, 1], [1, 0, 1]], 73 | [PAIRS[1]]: [[1, -1, -1], [1, 0, -1]], 74 | [PAIRS[2]]: [[-1, -1, -1], [-1, 0, -1]], 75 | [PAIRS[3]]: [[-1, -1, 1], [-1, 0, 1]] 76 | } 77 | 78 | export const BASE_POSITIONS = [ 79 | // Front face 80 | -1.0, -1.0, 1.0, 81 | 1.0, -1.0, 1.0, 82 | 1.0, 1.0, 1.0, 83 | -1.0, 1.0, 1.0, 84 | 85 | // Back face 86 | -1.0, -1.0, -1.0, 87 | -1.0, 1.0, -1.0, 88 | 1.0, 1.0, -1.0, 89 | 1.0, -1.0, -1.0, 90 | 91 | // Top face 92 | -1.0, 1.0, -1.0, 93 | -1.0, 1.0, 1.0, 94 | 1.0, 1.0, 1.0, 95 | 1.0, 1.0, -1.0, 96 | 97 | // Bottom face 98 | -1.0, -1.0, -1.0, 99 | 1.0, -1.0, -1.0, 100 | 1.0, -1.0, 1.0, 101 | -1.0, -1.0, 1.0, 102 | 103 | // Right face 104 | 1.0, -1.0, -1.0, 105 | 1.0, 1.0, -1.0, 106 | 1.0, 1.0, 1.0, 107 | 1.0, -1.0, 1.0, 108 | 109 | // Left face 110 | -1.0, -1.0, -1.0, 111 | -1.0, -1.0, 1.0, 112 | -1.0, 1.0, 1.0, 113 | -1.0, 1.0, -1.0 114 | ] 115 | 116 | export const Y_ROTATE_MAPPING = { 117 | '0': { '0': { '-1': 2, '1': 0 } }, 118 | '1': { '0': { '0': 3 }, '1': { '1': 0, '-1': 3 } }, 119 | '-1': { '0': { '0': 1 }, '1': { '1': 1, '-1': 2 } } 120 | } 121 | 122 | export const EDGE_GRID_MAPPING = { 123 | [S]: [0, 1, 1], [E]: [1, 1, 0], [N]: [0, 1, -1], [W]: [-1, 1, 0] 124 | } 125 | 126 | export const CORNER_GRID_MAPPING = { 127 | [SE]: [1, 1, 1], [NE]: [1, 1, -1], [NW]: [-1, 1, -1], [SW]: [-1, 1, 1] 128 | } 129 | 130 | export const GRID_MAPPING = Object.assign( 131 | {}, EDGE_GRID_MAPPING, CORNER_GRID_MAPPING 132 | ) 133 | 134 | export const TOP_BLOCKS = [ 135 | [0, 1, 1], [1, 1, 0], [0, 1, -1], [-1, 1, 0], 136 | [1, 1, 1], [1, 1, -1], [-1, 1, -1], [-1, 1, 1] 137 | ] 138 | 139 | export const TOP_FACE_MAPPING = { 140 | [NW]: [L, B], 141 | [NE]: [B, R], 142 | [SE]: [R, F], 143 | [SW]: [F, L], 144 | [N]: [B], 145 | [E]: [R], 146 | [S]: [F], 147 | [W]: [L] 148 | } 149 | 150 | export const INIT_BLOCKS = () => { 151 | const BLOCK_WIDTH = 1.8 152 | const BLOCK_MARGIN = 0.1 153 | const positionsFromCoords = (x, y, z) => { 154 | const diff = BLOCK_WIDTH + 2 * BLOCK_MARGIN 155 | const positions = BASE_POSITIONS 156 | .map((v, i) => v * BLOCK_WIDTH / 2 + [x, y, z][i % 3] * diff) 157 | return positions 158 | } 159 | const colorsFromCoords = (x, y, z) => { 160 | const colors = [...Array(6)].map(() => COLORS.EMPTY) 161 | FACE_MAPPING[x][y][z].forEach(i => { colors[i] = FACES[i] }) 162 | return colors 163 | } 164 | const blocks = [] 165 | for (let x = -1; x <= 1; x++) { 166 | for (let y = -1; y <= 1; y++) { 167 | for (let z = -1; z <= 1; z++) { 168 | blocks.push({ 169 | positions: positionsFromCoords(x, y, z), 170 | colors: colorsFromCoords(x, y, z) 171 | }) 172 | } 173 | } 174 | } 175 | return blocks 176 | } 177 | -------------------------------------------------------------------------------- /src/model/cube.js: -------------------------------------------------------------------------------- 1 | import { initProgram } from '../renderer/shaders.js' 2 | import { getBuffer } from '../renderer/buffer.js' 3 | import { renderFrame } from '../renderer/render.js' 4 | import { F, B, U, D, R, L, INIT_BLOCKS } from './consts.js' 5 | 6 | export class Cube { 7 | constructor (canvas, moves = []) { 8 | [this.rX, this.rY, this.__ANIMATING] = [0, 0, false] 9 | this.blocks = INIT_BLOCKS() 10 | this.moves = [] 11 | moves.forEach(n => this.move(n)) 12 | 13 | if (!canvas) return 14 | this.gl = canvas.getContext('webgl', { preserveDrawingBuffer: true }) 15 | this.programInfo = initProgram(this.gl) 16 | this.gl.useProgram(this.programInfo.program) 17 | } 18 | 19 | animate (move = null, duration = 500) { 20 | if (move && move.length === 0) return Promise.resolve() 21 | if (!move || this.__ANIMATING) throw new Error('Unable to animate!') 22 | 23 | // Recursively calling with poped moves, then animate from "inner" to here. 24 | if (Array.isArray(move) && move.length > 1) { 25 | const lastMove = move.pop() 26 | return this.animate(move).then(() => this.animate(lastMove)) 27 | } else if (move.length === 1) move = move[0] 28 | 29 | this.__ANIMATING = true 30 | let k = move.includes("'") ? 1 : -1 31 | if (/B|D|L/.test(move)) k = k * -1 32 | const beginTime = +new Date() 33 | return new Promise((resolve, reject) => { 34 | const tick = () => { 35 | const diff = +new Date() - beginTime 36 | const percentage = diff / duration 37 | const face = move.replace("'", '') 38 | if (percentage < 1) { 39 | this.render(this.rX, this.rY, face, 90 * percentage * k) 40 | window.requestAnimationFrame(tick) 41 | } else { 42 | this.move(move) 43 | this.render(this.rX, this.rY, null, 0) 44 | this.__ANIMATING = false 45 | resolve() 46 | } 47 | } 48 | window.requestAnimationFrame(tick) 49 | }) 50 | } 51 | 52 | getBlock ([x, y, z]) { 53 | return this.blocks[(x + 1) * 9 + (y + 1) * 3 + z + 1] 54 | } 55 | 56 | move (m) { 57 | if (Array.isArray(m)) { m.forEach(m => this.move(m)); return this } 58 | 59 | const mapping = { 60 | 'F': () => this.rotate([0, 0, 1], true), 61 | "F'": () => this.rotate([0, 0, 1], false), 62 | 'B': () => this.rotate([0, 0, -1], true), 63 | "B'": () => this.rotate([0, 0, -1], false), 64 | 'R': () => this.rotate([1, 0, 0], true), 65 | "R'": () => this.rotate([1, 0, 0], false), 66 | 'L': () => this.rotate([-1, 0, 0], true), 67 | "L'": () => this.rotate([-1, 0, 0], false), 68 | 'U': () => this.rotate([0, 1, 0], true), 69 | "U'": () => this.rotate([0, 1, 0], false), 70 | 'D': () => this.rotate([0, -1, 0], true), 71 | "D'": () => this.rotate([0, -1, 0], false) 72 | } 73 | mapping[m] && mapping[m](); this.moves.push(m) 74 | return this 75 | } 76 | 77 | render (rX = 0, rY = 0, moveFace = null, moveAngle = 0) { 78 | if (!this.gl) throw new Error('Missing WebGL context!') 79 | this.buffer = getBuffer(this.gl, this.blocks, moveFace, moveAngle) 80 | renderFrame(this.gl, this.programInfo, this.buffer, rX, rY) 81 | } 82 | 83 | rotate (center, clockwise = true) { 84 | const axis = center.indexOf(1) + center.indexOf(-1) + 1 85 | // Fix y direction in right-handed coordinate system. 86 | clockwise = center[1] !== 0 ? !clockwise : clockwise 87 | // Fix directions whose faces are opposite to axis. 88 | clockwise = center[axis] === 1 ? clockwise : !clockwise 89 | 90 | let cs = [[1, 1], [1, -1], [-1, -1], [-1, 1]] // corner coords 91 | let es = [[0, 1], [1, 0], [0, -1], [-1, 0]] // edge coords 92 | const prepareCoord = coord => coord.splice(axis, 0, center[axis]) 93 | cs.forEach(prepareCoord); es.forEach(prepareCoord) 94 | if (!clockwise) { cs = cs.reverse(); es = es.reverse() } 95 | 96 | // Rotate affected edge and corner blocks. 97 | const rotateBlocks = ([a, b, c, d]) => { 98 | const set = (a, b) => { for (let i = 0; i < 6; i++) a[i] = b[i] } 99 | const tmp = []; set(tmp, a); set(a, d); set(d, c); set(c, b); set(b, tmp) 100 | } 101 | const colorsAt = coord => this.getBlock(coord).colors 102 | rotateBlocks(cs.map(colorsAt)); rotateBlocks(es.map(colorsAt)) 103 | 104 | // Rotate all block faces with same rotation. 105 | const swap = [ 106 | [[F, U, B, D], [L, F, R, B], [L, U, R, D]], 107 | [[F, D, B, U], [F, L, B, R], [D, R, U, L]] 108 | ][clockwise ? 0 : 1][axis] 109 | const rotateFaces = coord => { 110 | const block = colorsAt(coord) 111 | ;[block[swap[1]], block[swap[2]], block[swap[3]], block[swap[0]]] = 112 | [block[swap[0]], block[swap[1]], block[swap[2]], block[swap[3]]] 113 | } 114 | cs.forEach(rotateFaces); es.forEach(rotateFaces) 115 | return this 116 | } 117 | 118 | shuffle (n = 20, animate = false) { 119 | const notations = ['F', 'B', 'U', 'D', 'R', 'L'] 120 | const runner = animate ? this.animate.bind(this) : this.move.bind(this) 121 | return runner( 122 | [...Array(n)].map(() => notations[parseInt(Math.random() * 6)]) 123 | ) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/renderer/math.js: -------------------------------------------------------------------------------- 1 | // Inline from gl-matrix 2 | // https://github.com/toji/gl-matrix 3 | 4 | const ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array 5 | const EPSILON = 0.000001 6 | 7 | export function create () { 8 | let out = new ARRAY_TYPE(16) 9 | if (ARRAY_TYPE !== Float32Array) { 10 | out[1] = 0 11 | out[2] = 0 12 | out[3] = 0 13 | out[4] = 0 14 | out[6] = 0 15 | out[7] = 0 16 | out[8] = 0 17 | out[9] = 0 18 | out[11] = 0 19 | out[12] = 0 20 | out[13] = 0 21 | out[14] = 0 22 | } 23 | out[0] = 1 24 | out[5] = 1 25 | out[10] = 1 26 | out[15] = 1 27 | return out 28 | } 29 | 30 | export function perspective (out, fovy, aspect, near, far) { 31 | let f = 1.0 / Math.tan(fovy / 2) 32 | let nf 33 | out[0] = f / aspect 34 | out[1] = 0 35 | out[2] = 0 36 | out[3] = 0 37 | out[4] = 0 38 | out[5] = f 39 | out[6] = 0 40 | out[7] = 0 41 | out[8] = 0 42 | out[9] = 0 43 | out[11] = -1 44 | out[12] = 0 45 | out[13] = 0 46 | out[15] = 0 47 | if (far != null && far !== Infinity) { 48 | nf = 1 / (near - far) 49 | out[10] = (far + near) * nf 50 | out[14] = (2 * far * near) * nf 51 | } else { 52 | out[10] = -1 53 | out[14] = -2 * near 54 | } 55 | return out 56 | } 57 | 58 | export function translate (out, a, [x, y, z]) { 59 | let a00, a01, a02, a03 60 | let a10, a11, a12, a13 61 | let a20, a21, a22, a23 62 | 63 | if (a === out) { 64 | out[12] = a[0] * x + a[4] * y + a[8] * z + a[12] 65 | out[13] = a[1] * x + a[5] * y + a[9] * z + a[13] 66 | out[14] = a[2] * x + a[6] * y + a[10] * z + a[14] 67 | out[15] = a[3] * x + a[7] * y + a[11] * z + a[15] 68 | } else { 69 | a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3] 70 | a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7] 71 | a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11] 72 | 73 | out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03 74 | out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13 75 | out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23 76 | 77 | out[12] = a00 * x + a10 * y + a20 * z + a[12] 78 | out[13] = a01 * x + a11 * y + a21 * z + a[13] 79 | out[14] = a02 * x + a12 * y + a22 * z + a[14] 80 | out[15] = a03 * x + a13 * y + a23 * z + a[15] 81 | } 82 | 83 | return out 84 | } 85 | 86 | export function rotate (out, a, rad, [x, y, z]) { 87 | let len = Math.sqrt(x * x + y * y + z * z) 88 | let s, c, t 89 | let a00, a01, a02, a03 90 | let a10, a11, a12, a13 91 | let a20, a21, a22, a23 92 | let b00, b01, b02 93 | let b10, b11, b12 94 | let b20, b21, b22 95 | 96 | if (len < EPSILON) { return null } 97 | 98 | len = 1 / len 99 | x *= len 100 | y *= len 101 | z *= len 102 | 103 | s = Math.sin(rad) 104 | c = Math.cos(rad) 105 | t = 1 - c 106 | 107 | a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3] 108 | a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7] 109 | a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11] 110 | 111 | // Construct the elements of the rotation matrix 112 | b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s 113 | b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s 114 | b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c 115 | 116 | // Perform rotation-specific matrix multiplication 117 | out[0] = a00 * b00 + a10 * b01 + a20 * b02 118 | out[1] = a01 * b00 + a11 * b01 + a21 * b02 119 | out[2] = a02 * b00 + a12 * b01 + a22 * b02 120 | out[3] = a03 * b00 + a13 * b01 + a23 * b02 121 | out[4] = a00 * b10 + a10 * b11 + a20 * b12 122 | out[5] = a01 * b10 + a11 * b11 + a21 * b12 123 | out[6] = a02 * b10 + a12 * b11 + a22 * b12 124 | out[7] = a03 * b10 + a13 * b11 + a23 * b12 125 | out[8] = a00 * b20 + a10 * b21 + a20 * b22 126 | out[9] = a01 * b20 + a11 * b21 + a21 * b22 127 | out[10] = a02 * b20 + a12 * b21 + a22 * b22 128 | out[11] = a03 * b20 + a13 * b21 + a23 * b22 129 | 130 | if (a !== out) { // If the source and destination differ, copy the unchanged last row 131 | out[12] = a[12] 132 | out[13] = a[13] 133 | out[14] = a[14] 134 | out[15] = a[15] 135 | } 136 | return out 137 | } 138 | 139 | export function rotateX (out, a, b, c) { 140 | let [p, r] = [[], []] 141 | // Translate point to the origin 142 | p[0] = a[0] - b[0] 143 | p[1] = a[1] - b[1] 144 | p[2] = a[2] - b[2] 145 | 146 | // perform rotation 147 | r[0] = p[0] 148 | r[1] = p[1] * Math.cos(c) - p[2] * Math.sin(c) 149 | r[2] = p[1] * Math.sin(c) + p[2] * Math.cos(c) 150 | 151 | // translate to correct position 152 | out[0] = r[0] + b[0] 153 | out[1] = r[1] + b[1] 154 | out[2] = r[2] + b[2] 155 | 156 | return out 157 | } 158 | 159 | export function rotateY (out, a, b, c) { 160 | let [p, r] = [[], []] 161 | // Translate point to the origin 162 | p[0] = a[0] - b[0] 163 | p[1] = a[1] - b[1] 164 | p[2] = a[2] - b[2] 165 | 166 | // perform rotation 167 | r[0] = p[2] * Math.sin(c) + p[0] * Math.cos(c) 168 | r[1] = p[1] 169 | r[2] = p[2] * Math.cos(c) - p[0] * Math.sin(c) 170 | 171 | // translate to correct position 172 | out[0] = r[0] + b[0] 173 | out[1] = r[1] + b[1] 174 | out[2] = r[2] + b[2] 175 | 176 | return out 177 | } 178 | 179 | export function rotateZ (out, a, b, c) { 180 | let [p, r] = [[], []] 181 | // Translate point to the origin 182 | p[0] = a[0] - b[0] 183 | p[1] = a[1] - b[1] 184 | p[2] = a[2] - b[2] 185 | 186 | // perform rotation 187 | r[0] = p[0] * Math.cos(c) - p[1] * Math.sin(c) 188 | r[1] = p[0] * Math.sin(c) + p[1] * Math.cos(c) 189 | r[2] = p[2] 190 | 191 | // translate to correct position 192 | out[0] = r[0] + b[0] 193 | out[1] = r[1] + b[1] 194 | out[2] = r[2] + b[2] 195 | 196 | return out 197 | } 198 | -------------------------------------------------------------------------------- /src/model/solver.js: -------------------------------------------------------------------------------- 1 | import { Cube } from './cube.js' 2 | import { 3 | F, B, U, D, R, L, SLOT_M, SLOT_D, 4 | S, E, N, W, SE, NE, NW, SW, 5 | COLORS, COLOR_D, COLOR_U, PAIRS, EDGE_COORDS, BLOCK_COORDS, TOP_BLOCKS, 6 | Y_ROTATE_MAPPING, SLOT_COORDS_MAPPING, EDGE_GRID_MAPPING, CORNER_GRID_MAPPING, 7 | GRID_MAPPING, TOP_FACE_MAPPING, EDGE_GRIDS, INIT_BLOCKS 8 | } from './consts.js' 9 | import * as RULES from './rules.js' 10 | 11 | const base = INIT_BLOCKS() // base cube blocks 12 | const baseBlockAt = ([x, y, z]) => base[(x + 1) * 9 + (y + 1) * 3 + z + 1] 13 | 14 | const isCorner = ([x, y, z]) => Math.abs(x) + Math.abs(y) + Math.abs(z) === 3 15 | 16 | const coordEqual = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2] 17 | 18 | const colorsEqual = (a, b) => a.colors.every((c, i) => c === b.colors[i]) 19 | 20 | const blockHasColor = (block, color) => block.colors.some(c => c === color) 21 | 22 | const isPairSolved = (cube, pair) => { 23 | return SLOT_COORDS_MAPPING[pair].every( 24 | coord => colorsEqual(cube.getBlock(coord), baseBlockAt(coord)) 25 | ) 26 | } 27 | 28 | export const isOrientationSolved = (cube) => { 29 | return TOP_BLOCKS.every(coord => cube.getBlock(coord).colors[U] === COLOR_U) 30 | } 31 | 32 | export const isPermutationSolved = (colors) => ( 33 | colors[NW][B] === colors[N][B] && colors[N][B] === colors[NE][B] && 34 | colors[NE][R] === colors[E][R] & colors[E][R] === colors[SE][R] && 35 | colors[SE][F] === colors[S][F] && colors[S][F] === colors[SW][F] && 36 | colors[SW][L] === colors[W][L] && colors[W][L] === colors[SW][L] 37 | ) 38 | 39 | const isTopLayerAligned = (cube) => { 40 | return colorsEqual(cube.getBlock(TOP_BLOCKS[0]), baseBlockAt(TOP_BLOCKS[0])) 41 | } 42 | 43 | const findCrossCoords = cube => EDGE_COORDS.filter(coord => { 44 | const block = cube.getBlock(coord) 45 | return ( 46 | blockHasColor(block, COLOR_D) && 47 | !colorsEqual(block, baseBlockAt(coord)) 48 | ) 49 | }) 50 | 51 | const findPairCoords = (cube, pair) => BLOCK_COORDS.filter(coord => { 52 | const block = cube.getBlock(coord) 53 | const faces = isCorner(coord) ? [...pair, COLOR_D] : pair 54 | return faces.every(c => block.colors.includes(c)) 55 | }) 56 | 57 | const inPairSlot = (coord, pair) => { 58 | const targetSlot = SLOT_COORDS_MAPPING[pair] 59 | return targetSlot.some(slotCoord => coordEqual(coord, slotCoord)) 60 | } 61 | 62 | const preparePair = (cube, pair, moves = []) => { 63 | const [edgeCoord, cornerCoord] = findPairCoords(cube, pair) 64 | let sideMoves = ['R', 'U', "R'"] 65 | if (!inPairSlot(edgeCoord, pair) && edgeCoord[1] === 0) { 66 | const tmpVec = [edgeCoord[0], 1, edgeCoord[2]] 67 | sideMoves = movesRelativeTo(tmpVec, sideMoves); cube.move(sideMoves) 68 | return preparePair(cube, pair, [...moves, ...sideMoves]) 69 | } 70 | if (!inPairSlot(cornerCoord, pair) && cornerCoord[1] === -1) { 71 | const tmpVec = [cornerCoord[0], 1, cornerCoord[2]] 72 | sideMoves = movesRelativeTo(tmpVec, sideMoves); cube.move(sideMoves) 73 | return preparePair(cube, pair, [...moves, ...sideMoves]) 74 | } 75 | return moves 76 | } 77 | 78 | const getTopColors = (cube) => { 79 | const result = [] 80 | TOP_BLOCKS.forEach(coord => result.push([...cube.getBlock(coord).colors])) 81 | return result 82 | } 83 | 84 | const getTopFaceMove = ([x0, y0, z0], [x1, y1, z1]) => { 85 | const offsetMapping = { 86 | '0': { '1': { '1': 0, '-1': 2 } }, 87 | '-1': { '1': { '0': 3 } }, 88 | '1': { '1': { '0': 1 } } 89 | } 90 | const fromOffset = offsetMapping[x0][y0][z0] 91 | const toOffset = offsetMapping[x1][y1][z1] 92 | const moveMapping = { 93 | '-3': ["U'"], 94 | '-2': ['U', 'U'], 95 | '-1': ['U'], 96 | '0': [], 97 | '1': ["U'"], 98 | '2': ['U', 'U'], 99 | '3': ['U'] 100 | } 101 | return moveMapping[toOffset - fromOffset] 102 | } 103 | 104 | const movesRelativeTo = (coord, moves) => { 105 | const moveRelativeTo = ([x, y, z], move) => { 106 | const base = ['F', 'L', 'B', 'R'] 107 | const baseIndex = base.indexOf(move.replace("'", '')) 108 | if (baseIndex === -1) return move 109 | let newNotation = base[(baseIndex + Y_ROTATE_MAPPING[x][y][z]) % 4] 110 | newNotation += move.includes("'") ? "'" : '' 111 | return newNotation 112 | } 113 | return moves.map(move => moveRelativeTo(coord, move)) 114 | } 115 | 116 | const faceRelativeTo = ([x, y, z], face) => { 117 | if (face === U || face === D) return face 118 | const base = [F, L, B, R] 119 | const baseIndex = base.indexOf(face) 120 | return base[(baseIndex + Y_ROTATE_MAPPING[x][y][z]) % 4] 121 | } 122 | 123 | const edgeRelativeTo = (vec, dir = 'left') => { 124 | return { 125 | [[1, 1, 1]]: [[0, 1, 1], [1, 1, 0]], 126 | [[1, 1, -1]]: [[1, 1, 0], [0, 1, -1]], 127 | [[-1, 1, -1]]: [[0, 1, -1], [-1, 1, 0]], 128 | [[-1, 1, 1]]: [[-1, 1, 0], [0, 1, 1]] 129 | }[vec][dir === 'left' ? 0 : 1] 130 | } 131 | 132 | const centerByColor = (color) => { 133 | return { 134 | [COLORS.BLUE]: [0, 0, 1], 135 | [COLORS.GREEN]: [0, 0, -1], 136 | [COLORS.RED]: [1, 0, 0], 137 | [COLORS.ORANGE]: [-1, 0, 0], 138 | [COLORS.YELLOW]: [0, 1, 0], 139 | [COLORS.WHITE]: [0, -1, 0] 140 | }[color] 141 | } 142 | 143 | const gridByPair = (pair, gridDir) => { 144 | gridDir = parseInt(gridDir) 145 | if (gridDir === SLOT_M) return SLOT_COORDS_MAPPING[pair][1] 146 | else if (gridDir === SLOT_D) return SLOT_COORDS_MAPPING[pair][0] 147 | const isEdge = gridDir < 4 148 | const index = isEdge 149 | ? (gridDir + PAIRS.indexOf(pair)) % 4 150 | : ((gridDir - 4 + PAIRS.indexOf(pair)) % 4) + 4 151 | const mapping = EDGE_GRIDS.includes(gridDir) 152 | ? EDGE_GRID_MAPPING : CORNER_GRID_MAPPING 153 | return mapping[index] 154 | } 155 | 156 | const matchPairRule = (cube, rule, pair) => { 157 | return Object.keys(rule.match).every(dir => { 158 | const ruleFaces = Object.keys(rule.match[dir]).map(x => parseInt(x)) 159 | const targetBlock = cube.getBlock(gridByPair(pair, dir)) 160 | const topCornerCoord = gridByPair(pair, SE) 161 | const mappedFaces = ruleFaces.map(f => faceRelativeTo(topCornerCoord, f)) 162 | const result = ruleFaces.every((face, i) => { 163 | const ruleColor = rule.match[dir][face] 164 | const expectedColor = ruleColor === COLOR_D ? COLOR_D : pair[ruleColor] 165 | return targetBlock.colors[mappedFaces[i]] === expectedColor 166 | }) 167 | return result 168 | }) 169 | } 170 | 171 | const tryPairRules = (cube, pair) => { 172 | if (isPairSolved(cube, pair)) return [] 173 | 174 | const preMoves = preparePair(new Cube(null, cube.moves), pair) 175 | const topMoves = [[], ['U'], ['U', 'U'], ["U'"]] 176 | for (let i = 0; i < topMoves.length; i++) { 177 | const testCube = new Cube( 178 | null, [...cube.moves, ...preMoves, ...topMoves[i]] 179 | ) 180 | for (let j = 0; j < RULES.F2L.length; j++) { 181 | if (matchPairRule(testCube, RULES.F2L[j], pair)) { 182 | const topCornerCoord = gridByPair(pair, SE) 183 | const mappedMoves = movesRelativeTo(topCornerCoord, RULES.F2L[j].moves) 184 | const result = [...preMoves, ...topMoves[i], ...mappedMoves] 185 | cube.move(result) 186 | if (!isPairSolved(cube, pair)) console.error(`Error F2L at ${j}`) 187 | return result 188 | } 189 | } 190 | } 191 | return null // rule not found 192 | } 193 | 194 | export const matchOrientationRule = (cube, { match }) => { 195 | return [S, E, N, W, SE, NE, NW, SW].every(dir => { 196 | const faceIndex = Number.isInteger(match[dir]) ? match[dir] : U 197 | return cube.getBlock(GRID_MAPPING[dir]).colors[faceIndex] === COLOR_U 198 | }) 199 | } 200 | 201 | const tryOrientationRules = (cube) => { 202 | if (isOrientationSolved(cube)) return [] 203 | 204 | const topMoves = [[], ['U'], ['U', 'U'], ["U'"]] 205 | for (let i = 0; i < topMoves.length; i++) { 206 | const testCube = new Cube(null, [...cube.moves, ...topMoves[i]]) 207 | for (let j = 0; j < RULES.OLL.length; j++) { 208 | const rule = RULES.OLL[j] 209 | if (matchOrientationRule(testCube, rule)) { 210 | const result = [...topMoves[i], ...rule.moves] 211 | cube.move(result) 212 | if (!isOrientationSolved(cube)) console.error(`Error OLL id ${rule.id}`) 213 | return result 214 | } 215 | } 216 | } 217 | return null // rule not found 218 | } 219 | 220 | export const matchPermutationRule = (cube, { match }) => { 221 | const oldColors = getTopColors(cube); const newColors = getTopColors(cube) 222 | Object.keys(match).forEach(fromGrid => { 223 | const toGrid = match[fromGrid] 224 | for (let i = 0; i < TOP_FACE_MAPPING[fromGrid].length; i++) { 225 | newColors[toGrid][TOP_FACE_MAPPING[toGrid][i]] = 226 | oldColors[fromGrid][TOP_FACE_MAPPING[fromGrid][i]] 227 | } 228 | }) 229 | return isPermutationSolved(newColors) 230 | } 231 | 232 | const tryPermutationRules = (cube) => { 233 | if (isPermutationSolved(getTopColors(cube))) return [] 234 | 235 | const preMoves = [[], ['U'], ['U', 'U'], ["U'"]] 236 | let solveMoves = []; let matched = false 237 | for (let i = 0; i < preMoves.length; i++) { 238 | const testCube = new Cube(null, [...cube.moves, ...preMoves[i]]) 239 | for (let j = 0; j < RULES.PLL.length; j++) { 240 | if (matchPermutationRule(testCube, RULES.PLL[j])) { 241 | solveMoves = [...preMoves[i], ...RULES.PLL[j].moves] 242 | cube.move(solveMoves) 243 | matched = true 244 | if (!isPermutationSolved(getTopColors(cube))) { 245 | console.error(`Error PLL name ${RULES.PLL[j].name}`) 246 | } 247 | break 248 | } 249 | } 250 | } 251 | if (!matched) return null 252 | const postMoves = [[], ['U'], ['U', 'U'], ["U'"]] 253 | for (let i = 0; i < postMoves.length; i++) { 254 | const testCube = new Cube(null, [...cube.moves, ...postMoves[i]]) 255 | if (isTopLayerAligned(testCube)) { 256 | return [...solveMoves, ...postMoves[i]] 257 | } 258 | } 259 | return null 260 | } 261 | 262 | const topEdgeToBottom = (cube, edgeCoord) => { 263 | const moves = [] 264 | const topEdge = cube.getBlock(edgeCoord) 265 | const targetColor = topEdge.colors.find( 266 | c => c !== COLOR_D && c !== COLORS.EMPTY 267 | ) 268 | const toCenter = centerByColor(targetColor) 269 | const toTopEdge = [toCenter[0], toCenter[1] + 1, toCenter[2]] 270 | moves.push(...getTopFaceMove(edgeCoord, toTopEdge)) 271 | if (topEdge.colors[U] !== COLOR_D) { 272 | moves.push(...movesRelativeTo(toCenter, ["U'", "R'", 'F', 'R'])) 273 | } else { 274 | moves.push(...movesRelativeTo(toCenter, ['F', 'F'])) 275 | } 276 | return moves 277 | } 278 | 279 | // TODO Clean up similar moves. 280 | const solveCrossEdge = (cube, [x, y, z]) => { 281 | const moves = [] 282 | const edge = cube.getBlock([x, y, z]) 283 | if (y === -1) { 284 | // Bottom layer 285 | const centerCoord = [x, y + 1, z] 286 | if (edge.colors[D] === COLOR_D) { 287 | const sideMoves = movesRelativeTo(centerCoord, ['F', 'F']) 288 | sideMoves.forEach(m => cube.move(m)) 289 | moves.push(...sideMoves) 290 | } else { 291 | const sideMoves = movesRelativeTo(centerCoord, ["F'", 'R', 'F', 'U']) 292 | sideMoves.forEach(m => cube.move(m)) 293 | moves.push(...sideMoves) 294 | } 295 | const movesToBottom = topEdgeToBottom(cube, [x, y + 2, z]) 296 | movesToBottom.forEach(m => cube.move(m)) 297 | moves.push(...movesToBottom) 298 | } else if (y === 0) { 299 | // Center layer 300 | const topVec = [x, y + 1, z] 301 | const leftFace = faceRelativeTo(topVec, F) 302 | if (edge.colors[leftFace] === COLOR_D) { 303 | const sideMoves = movesRelativeTo(topVec, ['R', 'U', "R'"]) 304 | sideMoves.forEach(m => cube.move(m)) 305 | moves.push(...sideMoves) 306 | } else { 307 | const sideMoves = movesRelativeTo(topVec, ["F'", "U'", 'F', 'U']) 308 | sideMoves.forEach(m => cube.move(m)) 309 | moves.push(...sideMoves) 310 | } 311 | const topEdge = edgeRelativeTo(topVec) 312 | const movesToBottom = topEdgeToBottom(cube, topEdge) 313 | movesToBottom.forEach(m => cube.move(m)) 314 | moves.push(...movesToBottom) 315 | } else { 316 | // Top layer 317 | const movesToBottom = topEdgeToBottom(cube, [x, y, z]) 318 | movesToBottom.forEach(m => cube.move(m)) 319 | moves.push(...movesToBottom) 320 | } 321 | return moves 322 | } 323 | 324 | export class Solver { 325 | constructor (cube) { 326 | this.cube = cube 327 | } 328 | 329 | solve () { 330 | this.solveCross().forEach(moves => this.cube.move(moves)) 331 | this.solveF2L().forEach(moves => this.cube.move(moves)) 332 | this.cube.move(this.solveOLL() || []) 333 | this.cube.move(this.solvePLL() || []) 334 | } 335 | 336 | solveCross () { 337 | const clonedCube = new Cube(null, this.cube.moves) 338 | const moveSteps = [] 339 | while (true) { 340 | const lostEdgeCoords = findCrossCoords(clonedCube) 341 | if (!lostEdgeCoords.length) break 342 | moveSteps.push(solveCrossEdge(clonedCube, lostEdgeCoords[0])) 343 | } 344 | return moveSteps 345 | } 346 | 347 | solveF2L () { 348 | const clonedCube = new Cube(null, this.cube.moves) 349 | const moveSteps = [] 350 | for (let i = 0; i < 4; i++) { 351 | moveSteps.push(tryPairRules(clonedCube, PAIRS[i])) 352 | } 353 | return moveSteps 354 | } 355 | 356 | solveOLL () { 357 | const clonedCube = new Cube(null, this.cube.moves) 358 | return tryOrientationRules(clonedCube) 359 | } 360 | 361 | solvePLL () { 362 | const clonedCube = new Cube(null, this.cube.moves) 363 | return tryPermutationRules(clonedCube) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/model/rules.js: -------------------------------------------------------------------------------- 1 | import { 2 | F, B, U, D, R, L, 3 | N, W, S, E, NW, NE, SW, SE, SLOT_M, SLOT_D, 4 | COLOR_D as CD, COLOR_F as CF, COLOR_R as CR 5 | } from './consts.js' 6 | 7 | const SE_D_AS_F = { [F]: CD, [U]: CF, [R]: CR } 8 | const SE_D_AS_R = { [F]: CF, [U]: CR, [R]: CD } 9 | const SE_D_AS_U = { [F]: CR, [U]: CD, [R]: CF } 10 | 11 | const SLOT_M_SOLVED = { [F]: CF, [R]: CR } 12 | const SLOT_M_REVERSED = { [F]: CR, [R]: CF } 13 | 14 | const SLOT_D_SOLVED = { [F]: CF, [R]: CR, [D]: CD } 15 | const SLOT_D_D_AS_F = { [F]: CD, [R]: CF, [D]: CR } 16 | const SLOT_D_D_AS_R = { [F]: CR, [R]: CD, [D]: CF } 17 | 18 | const topEdge = (topColor, dir) => { 19 | const mapping = { [W]: L, [N]: B, [E]: R, [S]: F } 20 | return { [U]: topColor, [mapping[dir]]: topColor === CF ? CR : CF } 21 | } 22 | 23 | // https://www.speedsolving.com/wiki/index.php/F2L 24 | export const F2L = [ 25 | // 1 26 | { 27 | match: { [E]: topEdge(CF, E), [SE]: SE_D_AS_F }, 28 | moves: "U (R U' R')" 29 | }, 30 | // 2 31 | { 32 | match: { [S]: topEdge(CR, S), [SE]: SE_D_AS_R }, 33 | moves: "U' (F' U F)" 34 | }, 35 | // 3 36 | { 37 | match: { [W]: topEdge(CR, W), [SE]: SE_D_AS_F }, 38 | moves: "F' U' F" 39 | }, 40 | // 4 41 | { 42 | match: { [N]: topEdge(CF, N), [SE]: SE_D_AS_R }, 43 | moves: "(R U R')" 44 | }, 45 | // 5 46 | { 47 | match: { [N]: topEdge(CF, N), [SE]: SE_D_AS_F }, 48 | moves: "(U' R U R') U (R' F R F')" 49 | }, 50 | // 6 51 | { 52 | match: { [W]: topEdge(CR, W), [SE]: SE_D_AS_R }, 53 | moves: "R R (B U B' U') R R" 54 | }, 55 | // 7 56 | { 57 | match: { [W]: topEdge(CF, W), [SE]: SE_D_AS_F }, 58 | moves: "U' (R U U R') U U (R U' R')" 59 | }, 60 | // 8 61 | { 62 | match: { [N]: topEdge(CR, N), [SE]: SE_D_AS_R }, 63 | moves: "(U F' U U F) (U F' U U F)" 64 | }, 65 | // 9 66 | { 67 | match: { [N]: topEdge(CR, N), [SE]: SE_D_AS_F }, 68 | moves: "(U F' U' F) U' (F' U' F)" 69 | }, 70 | // 10 71 | { 72 | match: { [W]: topEdge(CF, W), [SE]: SE_D_AS_R }, 73 | moves: "U' (R U R' U) (R U R')" 74 | }, 75 | // 11 76 | { 77 | match: { [E]: topEdge(CR, E), [SE]: SE_D_AS_F }, 78 | moves: "U (F R' F' R) (F R' F' R) U' R U R'" 79 | }, 80 | // 12 81 | { 82 | match: { [S]: topEdge(CF, S), [SE]: SE_D_AS_R }, 83 | moves: "R' U U R R U R R U R" 84 | }, 85 | // 13 86 | { 87 | match: { [S]: topEdge(CR, S), [SE]: SE_D_AS_F }, 88 | moves: "(U F' U F) U' (F' U' F)" 89 | }, 90 | // 14 91 | { 92 | match: { [E]: topEdge(CF, E), [SE]: SE_D_AS_R }, 93 | moves: "U' (R U' R' U) (R U R')" 94 | }, 95 | // 15 96 | { 97 | match: { [S]: topEdge(CF, S), [SE]: SE_D_AS_F }, 98 | moves: "R B L U' L' B' R'" 99 | }, 100 | // 16 101 | { 102 | match: { [E]: topEdge(CR, E), [SE]: SE_D_AS_R }, 103 | moves: "(R U' R') U U (F' U' F)" 104 | }, 105 | // 17 106 | { 107 | match: { [E]: topEdge(CF, E), [SE]: SE_D_AS_U }, 108 | moves: "(R U U R') U' (R U R')" 109 | }, 110 | // 18 111 | { 112 | match: { [S]: topEdge(CR, S), [SE]: SE_D_AS_U }, 113 | moves: "F' U U F U F' U' F" 114 | }, 115 | // 19 116 | { 117 | match: { [N]: topEdge(CF, N), [SE]: SE_D_AS_U }, 118 | moves: "U R U U R R (F R F')" 119 | }, 120 | // 20 121 | { 122 | match: { [W]: topEdge(CR, W), [SE]: SE_D_AS_U }, 123 | moves: "U' F' U U F F (R' F' R)" 124 | }, 125 | // 21 126 | { 127 | match: { [W]: topEdge(CF, W), [SE]: SE_D_AS_U }, 128 | moves: "R B U U B' R'" 129 | }, 130 | // 22 131 | { 132 | match: { [N]: topEdge(CR, N), [SE]: SE_D_AS_U }, 133 | moves: "F' L' U U L F" 134 | }, 135 | // 23 136 | { 137 | match: { [S]: topEdge(CF, S), [SE]: SE_D_AS_U }, 138 | moves: "U (F R' F' R) U (R U R')" 139 | }, 140 | // 24 141 | { 142 | match: { [E]: topEdge(CR, E), [SE]: SE_D_AS_U }, 143 | moves: "U F' L' U L F R U R'" 144 | }, 145 | // 25 146 | { 147 | match: { [E]: topEdge(CF, E), [SLOT_D]: SLOT_D_SOLVED }, 148 | moves: "U' (F' U F) U (R U' R')" 149 | }, 150 | // 26 151 | { 152 | match: { [S]: topEdge(CR, S), [SLOT_D]: SLOT_D_SOLVED }, 153 | moves: "U (R U' R') U' (F' U F)" 154 | }, 155 | // 27 156 | { 157 | match: { [E]: topEdge(CF, E), [SLOT_D]: SLOT_D_D_AS_F }, 158 | moves: "(R U' R' U) (R U' R')" 159 | }, 160 | // 28 161 | { 162 | match: { [S]: topEdge(CR, S), [SLOT_D]: SLOT_D_D_AS_R }, 163 | moves: "(R U R' U') F R' F' R" 164 | }, 165 | // 29 166 | { 167 | match: { [S]: topEdge(CR, S), [SLOT_D]: SLOT_D_D_AS_F }, 168 | moves: "(R' F R F') (R' F R F')" 169 | }, 170 | // 30 171 | { 172 | match: { [E]: topEdge(CF, E), [SLOT_D]: SLOT_D_D_AS_R }, 173 | moves: "(R U R' U') (R U R')" 174 | }, 175 | // 31 176 | { 177 | match: { [SLOT_M]: SLOT_M_REVERSED, [SE]: SE_D_AS_U }, 178 | moves: "(R U' R' U) (F' U F)" 179 | }, 180 | // 32 181 | { 182 | match: { [SLOT_M]: SLOT_M_SOLVED, [SE]: SE_D_AS_U }, 183 | moves: "(R U R' U') (R U R' U') (R U R')" 184 | }, 185 | // 33 186 | { 187 | match: { [SLOT_M]: SLOT_M_SOLVED, [SE]: SE_D_AS_F }, 188 | moves: "U' (R U' R') U U (R U' R')" 189 | }, 190 | // 34 191 | { 192 | match: { [SLOT_M]: SLOT_M_SOLVED, [SE]: SE_D_AS_R }, 193 | moves: "U (F' U F) U U (F' U F)" 194 | }, 195 | // 35 196 | { 197 | match: { [SLOT_M]: SLOT_M_REVERSED, [SE]: SE_D_AS_F }, 198 | moves: "U U (R U' R') U' (F' U' F)" 199 | }, 200 | // 36 201 | { 202 | match: { [SLOT_M]: SLOT_M_REVERSED, [SE]: SE_D_AS_R }, 203 | moves: "U F' U' F U' (R U R')" 204 | }, 205 | // 37 Solved 206 | // 38 207 | { 208 | match: { [SLOT_M]: SLOT_M_REVERSED, [SLOT_D]: SLOT_D_SOLVED }, 209 | moves: "(R' F R F') (R U' R' U) (R U' R' U U) (R U' R')" 210 | }, 211 | // 39 212 | { 213 | match: { [SLOT_M]: SLOT_M_SOLVED, [SLOT_D]: SLOT_D_D_AS_F }, 214 | moves: "(R U' R') U' (R U R') U U (R U' R')" 215 | }, 216 | // 40 217 | { 218 | match: { [SLOT_M]: SLOT_M_SOLVED, [SLOT_D]: SLOT_D_D_AS_R }, 219 | moves: "(R U' R' U) (R U U R') U (R U' R')" 220 | }, 221 | // 41 222 | { 223 | match: { [SLOT_M]: SLOT_M_REVERSED, [SLOT_D]: SLOT_D_D_AS_F }, 224 | moves: "R F (U R U' R' F') U' R'" 225 | }, 226 | // 42 227 | { 228 | match: { [SLOT_M]: SLOT_M_REVERSED, [SLOT_D]: SLOT_D_D_AS_R }, 229 | moves: "(R U R' U') (R U' R') U U (F' U' F)" 230 | } 231 | ].map(rule => ({ 232 | match: rule.match, 233 | moves: rule.moves.replace(/(\(|\))/g, '').split(' ') 234 | })) 235 | 236 | // https://www.speedsolving.com/wiki/index.php/OLL 237 | export const OLL = [ 238 | { 239 | id: 1, 240 | match: { 241 | [NW]: L, [N]: B, [NE]: R, [E]: R, [SE]: R, [S]: F, [SW]: L, [W]: L 242 | }, 243 | moves: "R U B' R B R R U' R' F R F'" 244 | }, 245 | { 246 | id: 2, 247 | match: { 248 | [NW]: L, [N]: B, [NE]: B, [E]: R, [SE]: F, [S]: F, [SW]: L, [W]: L 249 | }, 250 | moves: "U U F R' F' R U R R B' R' B U' R'" 251 | }, 252 | { 253 | id: 3, 254 | match: { [NW]: B, [N]: B, [E]: R, [SE]: F, [S]: F, [SW]: L, [W]: L }, 255 | moves: "L (U F U' F') L' U' R (B U B' U') R'" 256 | }, 257 | { 258 | id: 4, 259 | match: { [NW]: L, [N]: B, [E]: R, [SE]: R, [S]: F, [SW]: F, [W]: L }, 260 | moves: "B (U L U' L') B' U F (R U R' U') F'" 261 | }, 262 | { 263 | id: 5, 264 | match: { [NW]: B, [N]: B, [NE]: R, [SW]: L, [W]: L }, 265 | moves: "L' B B R B R' B L" 266 | }, 267 | { 268 | id: 6, 269 | match: { [NW]: L, [N]: B, [NE]: B, [E]: R, [SE]: R }, 270 | moves: "R B B L' B' L B' R'" 271 | }, 272 | { 273 | id: 7, 274 | match: { [NW]: B, [NE]: R, [E]: R, [SE]: F, [S]: F }, 275 | moves: "F R' F' R U U R U U R'" 276 | }, 277 | { 278 | id: 8, 279 | match: { [NW]: L, [NE]: B, [S]: F, [SW]: F, [W]: L }, 280 | moves: "R U U R' U U R' F R F'" 281 | }, 282 | { 283 | id: 9, 284 | match: { [NW]: L, [NE]: B, [SE]: R, [S]: F, [W]: L }, 285 | moves: "R' U' R U' R' U R' F R F' U R" 286 | }, 287 | { 288 | id: 10, 289 | match: { [NW]: B, [NE]: R, [E]: R, [S]: F, [SW]: L }, 290 | moves: "(F U F' U) (F' L F L') F U U F'" 291 | }, 292 | { 293 | id: 11, 294 | match: { [N]: B, [NE]: R, [E]: R, [SE]: F, [SW]: L }, 295 | moves: "R U' R' U' R U' R' U U F' U F U' R U R'" 296 | }, 297 | { 298 | id: 12, 299 | match: { [NW]: L, [N]: B, [SE]: R, [SW]: F, [W]: L }, 300 | moves: "F (R U R' U') F' U F (R U R' U') F'" 301 | }, 302 | { 303 | id: 13, 304 | match: { [NW]: B, [N]: B, [NE]: R, [SE]: F, [S]: F }, 305 | moves: "F U R U' R R F' R U R U' R'" 306 | }, 307 | { 308 | id: 14, 309 | match: { [NW]: L, [N]: B, [NE]: B, [S]: F, [SW]: F }, 310 | moves: "F' U' L' U U L U L' U' L F" 311 | }, 312 | { 313 | id: 15, 314 | match: { [NW]: B, [N]: B, [NE]: R, [S]: F, [SW]: L }, 315 | moves: "L' B' L R' U' R U L' B L" 316 | }, 317 | { 318 | id: 16, 319 | match: { [NW]: L, [N]: B, [NE]: B, [SE]: R, [S]: F }, 320 | moves: "R B R' (L U L' U') R B' R'" 321 | }, 322 | { 323 | id: 17, 324 | match: { [N]: B, [NE]: B, [E]: R, [S]: F, [SW]: L, [W]: L }, 325 | moves: "R U R' U (R' F R F') U U (R' F R F')" 326 | }, 327 | { 328 | id: 18, 329 | match: { [NW]: L, [N]: B, [E]: R, [S]: F, [SW]: L, [W]: L }, 330 | moves: "(L' B L B') U U (L' B L B') (U B' U B)" 331 | }, 332 | { 333 | id: 19, 334 | match: { [N]: B, [E]: R, [SE]: R, [S]: F, [SW]: L, [W]: L }, 335 | moves: "R' U U F R U R' U' F F U U F R" 336 | }, 337 | { 338 | id: 20, 339 | match: { [N]: B, [E]: R, [S]: F, [W]: L }, 340 | moves: "R B U B' R' F F B D' L' D B' F F" 341 | }, 342 | { 343 | id: 21, 344 | match: { [NW]: L, [NE]: R, [SE]: R, [SW]: L }, 345 | moves: "R U R' U R U' R' U R U U R'" 346 | }, 347 | { 348 | id: 22, 349 | match: { [NW]: L, [NE]: B, [SE]: F, [SW]: L }, 350 | moves: "R U U R' R' U' R R U' R' R' U U R" 351 | }, 352 | { 353 | id: 23, 354 | match: { [NW]: B, [NE]: B }, 355 | moves: "R' U U R F U' R' U' R U F'" 356 | }, 357 | { 358 | id: 24, 359 | match: { [NW]: B, [SW]: F }, 360 | moves: "L F R' F' L' F R F'" 361 | }, 362 | { 363 | id: 25, 364 | match: { [NE]: B, [SW]: L }, 365 | moves: "R' F' L' F R F' L F" 366 | }, 367 | { 368 | id: 26, 369 | match: { [NW]: L, [NE]: B, [SW]: F }, 370 | moves: "L' U' L U' L' U U L" 371 | }, 372 | { 373 | id: 27, 374 | match: { [NW]: B, [NE]: R, [SE]: F }, 375 | moves: "R U R' U R U U R'" 376 | }, 377 | { 378 | id: 28, 379 | match: { [E]: R, [S]: F }, 380 | moves: "F R U R' U' F F L' U' L U F" 381 | }, 382 | { 383 | id: 29, 384 | match: { [N]: B, [E]: R, [SE]: R, [SW]: L }, 385 | moves: "L' L' U' L B L' U L' L' U' L' B' L" 386 | }, 387 | { 388 | id: 30, 389 | match: { [N]: B, [SE]: R, [SW]: L, [W]: L }, 390 | moves: "R' R' U R' B' R U' R' R' U R B R'" 391 | }, 392 | { 393 | id: 31, 394 | match: { [N]: B, [NE]: B, [E]: R, [SE]: F }, 395 | moves: "L' U' B U L U' L' B' L" 396 | }, 397 | { 398 | id: 32, 399 | match: { [NW]: B, [N]: B, [SW]: F, [W]: L }, 400 | moves: "R U B' U' R' U R B R'" 401 | }, 402 | { 403 | id: 33, 404 | match: { [NW]: B, [N]: B, [S]: F, [SW]: F }, 405 | moves: "(R U R' U') (R' F R F')" 406 | }, 407 | { 408 | id: 34, 409 | match: { [NW]: L, [N]: B, [NE]: R, [S]: F }, 410 | moves: "(R U R' U') B' (R' F R F') B" 411 | }, 412 | { 413 | id: 35, 414 | match: { [N]: B, [NE]: R, [SW]: F, [W]: L }, 415 | moves: "R U U R' R' F R F' R U U R'" 416 | }, 417 | { 418 | id: 36, 419 | match: { [N]: B, [NE]: R, [E]: R, [SW]: F }, 420 | moves: "R' U' R U' R' U R U R B' R' B" 421 | }, 422 | { 423 | id: 37, 424 | match: { [NW]: B, [N]: B, [E]: R, [SE]: R }, 425 | moves: "R B' R' B U B U' B'" 426 | }, 427 | { 428 | id: 38, 429 | match: { [NW]: L, [N]: B, [SE]: F, [W]: L }, 430 | moves: "L U L' U L U' L' U' L' B L B'" 431 | }, 432 | { 433 | id: 39, 434 | match: { [NW]: B, [N]: B, [SE]: R, [S]: F }, 435 | moves: "L F' (L' U' L U) F U' L'" 436 | }, 437 | { 438 | id: 40, 439 | match: { [N]: B, [NE]: B, [S]: F, [SW]: L }, 440 | moves: "R' F (R U R' U') F' U R" 441 | }, 442 | { 443 | id: 41, 444 | match: { [N]: B, [SE]: F, [SW]: F, [W]: L }, 445 | moves: "B U L U' L' B' L' U U L U L' U L" 446 | }, 447 | { 448 | id: 42, 449 | match: { [N]: B, [E]: R, [SE]: F, [SW]: F }, 450 | moves: "R' U' R U F R U R' U' R' U R U' F'" 451 | }, 452 | { 453 | id: 43, 454 | match: { [N]: B, [NE]: R, [E]: R, [SE]: R }, 455 | moves: "B' U' R' U R B" 456 | }, 457 | { 458 | id: 44, 459 | match: { [NW]: L, [N]: B, [SW]: L, [W]: L }, 460 | moves: "B U L U' L' B'" 461 | }, 462 | { 463 | id: 45, 464 | match: { [NW]: L, [N]: B, [S]: F, [SW]: L }, 465 | moves: "F (R U R' U') F'" 466 | }, 467 | { 468 | id: 46, 469 | match: { [NE]: R, [E]: R, [SE]: R, [W]: L }, 470 | moves: "R' U' R' F R F' U R" 471 | }, 472 | { 473 | id: 47, 474 | match: { [NW]: B, [NE]: R, [W]: L, [SW]: F, [S]: F, [SE]: R }, 475 | moves: "F' (L' U' L U) (L' U' L U) F" 476 | }, 477 | { 478 | id: 48, 479 | match: { [NW]: L, [NE]: B, [E]: R, [SE]: F, [S]: F, [SW]: L }, 480 | moves: "F (R U R' U') (R U R' U') F'" 481 | }, 482 | { 483 | id: 49, 484 | match: { [NW]: B, [N]: B, [NE]: R, [E]: R, [SE]: R, [SW]: F }, 485 | moves: "R B' R R F R R B R R F' R" 486 | }, 487 | { 488 | id: 50, 489 | match: { [NW]: L, [N]: B, [NE]: B, [SE]: F, [SW]: L, [W]: L }, 490 | moves: "R B' R B R R U U F R' F' R" 491 | }, 492 | { 493 | id: 51, 494 | match: { [NW]: B, [N]: B, [NE]: R, [SE]: R, [S]: F, [SW]: F }, 495 | moves: "F U R U' R' U R U' R' F'" 496 | }, 497 | { 498 | id: 52, 499 | match: { [NW]: B, [NE]: R, [E]: R, [SE]: R, [SW]: F, [W]: L }, 500 | moves: "R' U' R U' R' U F' U F R" 501 | }, 502 | { 503 | id: 53, 504 | match: { [NW]: L, [SW]: L, [S]: F, [SE]: R, [E]: R, [NE]: R }, 505 | moves: "F R U R' U' F' R U R' U' R' F R F'" 506 | }, 507 | { 508 | id: 54, 509 | match: { [NW]: L, [NE]: R, [SE]: R, [S]: F, [SW]: L, [W]: L }, 510 | moves: "U U F R' F' R U U F F L F L' F" 511 | }, 512 | { 513 | id: 55, 514 | match: { [NW]: L, [NE]: R, [E]: R, [SE]: R, [SW]: L, [W]: L }, 515 | moves: "R U U R R U' R U' R' U U F R F'" 516 | }, 517 | { 518 | id: 56, 519 | match: { [NW]: L, [N]: B, [NE]: R, [SE]: R, [S]: F, [SW]: L }, 520 | moves: "L F L' U R U' R' U R U' R' L F' L'" 521 | }, 522 | { 523 | id: 57, 524 | match: { [N]: B, [S]: F }, 525 | moves: "R U R' U' L R' F R F' L'" 526 | } 527 | ].map(rule => ({ 528 | id: rule.id, 529 | match: rule.match, 530 | moves: rule.moves.replace(/(\(|\))/g, '').split(' ') 531 | })) 532 | 533 | // https://www.speedsolving.com/wiki/index.php/PLL 534 | export const PLL = [ 535 | { 536 | name: 'H', 537 | match: { [N]: S, [S]: [N], [W]: E, [E]: W }, 538 | moves: "L R U U L' R' F' B' U U F B" 539 | }, 540 | { 541 | name: 'U-a', 542 | match: { [N]: W, [W]: [E], [E]: N }, 543 | moves: "R R U' R' U' R U R U R U' R" 544 | }, 545 | { 546 | name: 'U-b', 547 | match: { [N]: E, [E]: W, [W]: N }, 548 | moves: "R' U R' U' R' U' R' U R U R R" 549 | }, 550 | { 551 | name: 'Z', 552 | match: { [W]: N, [N]: W, [S]: E, [E]: S }, 553 | moves: "U R' U' R U' R U R U' R' U R U R R U' R' U" 554 | }, 555 | { 556 | name: 'A-a', 557 | match: { [NW]: NE, [NE]: SE, [SE]: NW }, 558 | moves: "R' F R' B B R F' R' B B R R" 559 | }, 560 | { 561 | name: 'A-b', 562 | match: { [NE]: SW, [SW]: SE, [SE]: NE }, 563 | moves: "R B' R F F R' B R F F R R" 564 | }, 565 | { 566 | name: 'E', 567 | match: { [NW]: NE, [NE]: NW, [SW]: SE, [SE]: SW }, 568 | moves: "D R' D D F' D L D' F D D R D' F' L' F" 569 | }, 570 | { 571 | name: 'F', 572 | match: { [NW]: NE, [NE]: NW, [W]: E, [E]: W }, 573 | moves: "(R' U R U') R R (F' U' F U) (R F R' F') R R U'" 574 | }, 575 | { 576 | name: 'G-a', 577 | match: { [NW]: SE, [SE]: SW, [SW]: NW, [W]: S, [S]: N, [N]: W }, 578 | moves: "(R U R' U' R') U F (R U R U' R') F' U R' U U R" 579 | }, 580 | { 581 | name: 'G-b', 582 | match: { [NW]: SW, [SW]: SE, [SE]: NW, [W]: N, [N]: S, [S]: W }, 583 | moves: "R' U' R B B D (L' U L U' L) D' B B" 584 | }, 585 | { 586 | name: 'G-c', 587 | match: { [NE]: SW, [SW]: SE, [SE]: NE, [N]: E, [E]: S, [S]: N }, 588 | moves: "L' U' L U L U' F' L' U' L' U L F U' L U U L'" 589 | }, 590 | { 591 | name: 'G-d', 592 | match: { [NE]: SE, [SE]: SW, [SW]: NE, [N]: S, [S]: E, [E]: N }, 593 | moves: "L U L' B B D' (R U' R' U R') D B B" 594 | }, 595 | { 596 | name: 'J-a', 597 | match: { [NW]: SW, [SW]: NW, [W]: S, [S]: W }, 598 | moves: "(B' U F') U U (B U' B') U U (F B U')" 599 | }, 600 | { 601 | name: 'J-b', 602 | match: { [NE]: SE, [SE]: NE, [E]: S, [S]: E }, 603 | moves: "(B U' F) U U (B' U B) U U (F' B' U)" 604 | }, 605 | { 606 | name: 'N-a', 607 | match: { [W]: E, [E]: W, [SW]: NE, [NE]: SW }, 608 | moves: "(R U' L U U R' U L') (R U' L U U R' U L')" 609 | }, 610 | { 611 | name: 'N-b', 612 | match: { [W]: E, [E]: W, [NW]: SE, [SE]: NW }, 613 | moves: "(L' U R' U' U' L U' R) (L' U R' U' U' L U' R)" 614 | }, 615 | { 616 | name: 'R-a', 617 | match: { [N]: E, [E]: N, [SW]: SE, [SE]: SW }, 618 | moves: "R U U R' U U R B' R' U' R U R B R R" 619 | }, 620 | { 621 | name: 'R-b', 622 | match: { [NW]: NE, [NE]: NW, [S]: E, [E]: S }, 623 | moves: "R' U U R U U R' F R U R' U' R' F' R R" 624 | }, 625 | { 626 | name: 'T', 627 | match: { [W]: E, [E]: W, [NE]: SE, [SE]: NE }, 628 | moves: "R U R' U' R' F R R U' R' U' R U R' F'" 629 | }, 630 | { 631 | name: 'V', 632 | match: { [NW]: SE, [SE]: NW, [N]: E, [E]: N }, 633 | moves: "R' U R' U' B' R' B B U' B' U B' R B R" 634 | }, 635 | { 636 | name: 'Y', 637 | match: { [NW]: SE, [SE]: NW, [W]: N, [N]: W }, 638 | moves: "F R U' R' U' R U R' F' R U R' U' R' F R F'" 639 | } 640 | ].map(rule => ({ 641 | name: rule.name, 642 | match: rule.match, 643 | moves: rule.moves.replace(/(\(|\))/g, '').split(' ') 644 | })) 645 | --------------------------------------------------------------------------------