├── .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 | 
5 |
6 | Freecube renders and animates Rubik's Cube with raw WebGL, plus a tiny rule-based solver showing how CFOP works.
7 |
8 | 
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 |
--------------------------------------------------------------------------------