├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── coffeelint.json ├── docs ├── css │ ├── base.css │ ├── html5reset-1.6.css │ └── pygments.css ├── demo.html ├── demo.js ├── index.html └── lib │ ├── async.js │ ├── cube.js │ ├── solve.js │ └── worker.js ├── gulpfile.js ├── index.js ├── karma.conf.coffee ├── lib ├── async.js ├── cube.js ├── solve.js └── worker.js ├── package-lock.json ├── package.json ├── spec └── cube.spec.coffee └── src ├── async.coffee ├── cube.coffee ├── solve.coffee └── worker.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [{*.json,*.js,*.html,*.css,*.coffee}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Tests 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | - run: npm install 17 | - run: npm run test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | .idea/ 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | *.sublime* 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | *.sublime* 19 | 20 | docs 21 | gulpfile.js 22 | coffeelint.json 23 | .travis.yml 24 | .gitignore 25 | .editorconfig 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 Petri Lehtinen 2 | Copyright (c) 2018 Ludovic Fernandez 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cube.js 2 | 3 | [![Build Status](https://travis-ci.org/ldez/cubejs.svg?branch=master)](https://travis-ci.org/ldez/cubejs) 4 | [![npm version](https://badge.fury.io/js/cubejs.svg)](https://www.npmjs.com/package/cubejs) 5 | 6 | **cube.js** is a JavaScript library for modeling and solving the 3x3x3 Rubik's Cube. 7 | 8 | Most notably, it implements [Herbert Kociemba's two-phase algorithm](http://kociemba.org/cube.htm) for solving any state of the 9 | cube very fast in 22 moves or less. 10 | 11 | **cube.js** is written in [CoffeeScript](http://coffeescript.org/), and runs on [node.js](http://nodejs.org/) and modern browsers. 12 | 13 | A full-fledged random state scrambler demo is available [here](http://ldez.github.io/cubejs/). 14 | 15 | ## Examples 16 | 17 | `cube.js` gives you basic cube manipulation: 18 | 19 | - Web: 20 | 21 | ```javascript 22 | // Create a new solved cube instance 23 | const cube = new Cube(); 24 | 25 | // Apply an algorithm or randomize the cube state 26 | cube.move("U F R2 B' D2 L'"); 27 | cube.randomize(); 28 | 29 | // Create a new random cube 30 | const randomCube = Cube.random(); 31 | ``` 32 | 33 | - Node: 34 | 35 | ```javascript 36 | const Cube = require('cubejs'); 37 | 38 | // Create a new solved cube instance 39 | const cube = new Cube(); 40 | 41 | // Apply an algorithm or randomize the cube state 42 | cube.move("U F R2 B' D2 L'"); 43 | cube.randomize(); 44 | 45 | // Create a new random cube 46 | const randomCube = Cube.random(); 47 | ``` 48 | 49 | From `solve.js` you can use the methods below, to solve the cube using [Herbert Kociemba's two-phase algorithm](http://kociemba.org/cube.htm). 50 | 51 | - Web: 52 | 53 | ```javascript 54 | // This takes 4-5 seconds on a modern computer 55 | Cube.initSolver(); 56 | 57 | // These typically take from 0.01s to 0.4s, rarely up to 2s 58 | cube.solve(); // => "D2 B' R' B L' B ..." 59 | randomCube.solve(); // => "R B' R U' D' R' ..." 60 | ``` 61 | 62 | To offload the solving to a web worker, use `async.js` and `worker.js`: 63 | 64 | ```javascript 65 | Cube.asyncInit('lib/worker.js', function() { 66 | // Initialized 67 | Cube.asyncSolve(randomCube, function(algorithm) { 68 | console.log(algorithm); 69 | }); 70 | }); 71 | ``` 72 | 73 | - Node: 74 | 75 | ```javascript 76 | const Cube = require('cubejs'); 77 | 78 | // This takes 4-5 seconds on a modern computer 79 | Cube.initSolver(); 80 | 81 | // These typically take from 0.01s to 0.4s, rarely up to 2s 82 | cube.solve(); // => "D2 B' R' B L' B ..." 83 | randomCube.solve(); // => "R B' R U' D' R' ..." 84 | ``` 85 | 86 | 87 | ## API Reference 88 | 89 | All functionality is implemented in the `Cube` object. 90 | There are a bunch of files, of which `cube.js` is always required. 91 | 92 | ### `cube.js` 93 | 94 | `cube.js` gives you basic cube manipulation. 95 | 96 | #### Class methods 97 | 98 | ##### `Cube([state])` 99 | 100 | The constructor: 101 | - without arguments, constructs an identity cube (i.e. a solved cube). 102 | - with an argument, clones another cube or cube state. 103 | 104 | ```javascript 105 | const cube = new Cube(); 106 | const other = new Cube(cube); 107 | const third = new Cube(cube.toJSON()); 108 | ``` 109 | 110 | ##### `Cube.fromString(str)` 111 | 112 | Returns a cube that represents the given facelet string. 113 | The string consists of 54 characters, 9 per face: 114 | 115 | ```javascript 116 | "UUUUUUUUUR...F...D...L...B..." 117 | ``` 118 | 119 | `U` means a facelet of the up face color, `R` means a facelet of the right face color, etc. 120 | 121 | The following diagram demonstrates the order of the facelets: 122 | 123 | ``` 124 | +------------+ 125 | | U1 U2 U3 | 126 | | | 127 | | U4 U5 U6 | 128 | | | 129 | | U7 U8 U9 | 130 | +------------+------------+------------+------------+ 131 | | L1 L2 L3 | F1 F2 F3 | R1 R2 R3 | B1 B2 B3 | 132 | | | | | | 133 | | L4 L5 L6 | F4 F5 F6 | R4 R5 R6 | B4 B5 B6 | 134 | | | | | | 135 | | L7 L8 L9 | F7 F8 F9 | R7 R8 R9 | B7 B8 B9 | 136 | +------------+------------+------------+------------+ 137 | | D1 D2 D3 | 138 | | | 139 | | D4 D5 D6 | 140 | | | 141 | | D7 D8 D9 | 142 | +------------+ 143 | ``` 144 | 145 | ##### `Cube.random()` 146 | 147 | Return a new, randomized cube. 148 | 149 | ```javascript 150 | const cube = Cube.random(); 151 | ``` 152 | 153 | ##### `Cube.inverse(algoritm)` 154 | 155 | Given an algorithm (a string, array of moves, or a single move), returns its inverse. 156 | 157 | ```javascript 158 | Cube.inverse("F B' R"); // => "R' B F'" 159 | Cube.inverse([1, 8, 12]); // => [14, 6, 1] 160 | Cube.inverse(8); // => 6 161 | ``` 162 | 163 | See below for numeric moves. 164 | 165 | #### Instance methods 166 | 167 | ##### `init(state)` 168 | 169 | Resets the cube state to match another cube. 170 | 171 | ```javascript 172 | const random = Cube.random(); 173 | const cube = new Cube(); 174 | cube.init(random); 175 | ``` 176 | 177 | ##### `identity()` 178 | 179 | Resets the cube to the identity cube. 180 | 181 | ```javascript 182 | cube.identity(); 183 | cube.isSolved(); // => true 184 | ``` 185 | 186 | ##### `toJSON()` 187 | 188 | Returns the cube state as an object. 189 | 190 | ```javascript 191 | cube.toJSON(); // => {cp: [...], co: [...], ep: [...], eo: [...]} 192 | ``` 193 | 194 | ##### `asString()` 195 | 196 | Returns the cube's state as a facelet string. See `Cube.fromString()`. 197 | 198 | ##### `clone()` 199 | 200 | Returns a fresh clone of the cube. 201 | 202 | ##### `randomize()` 203 | 204 | Randomizes the cube in place. 205 | 206 | ##### `isSolved()` 207 | 208 | Returns `true` if the cube is solved (i.e. the identity cube), and `false` otherwise. 209 | 210 | ##### `move(algorithm)` 211 | 212 | Applies an algorithm (a string, array of moves, or a single move) to the cube. 213 | 214 | ```javascript 215 | const cube = new Cube(); 216 | cube.isSolved(); // => true 217 | cube.move("U R F'"); 218 | cube.isSolved(); // => false 219 | ``` 220 | 221 | See below for numeric moves. 222 | 223 | #### Numeric moves 224 | 225 | Internally, cube.js treats moves as numbers. 226 | 227 | 228 | | Move | Number | 229 | |------|--------| 230 | | U | 0 | 231 | | U2 | 1 | 232 | | U' | 2 | 233 | | R | 3 | 234 | | R2 | 4 | 235 | | R' | 5 | 236 | | F | 6 | 237 | | F2 | 7 | 238 | | F' | 8 | 239 | | D | 9 | 240 | | D2 | 10 | 241 | | D' | 11 | 242 | | L | 12 | 243 | | L2 | 13 | 244 | | L' | 14 | 245 | | B | 15 | 246 | | B2 | 16 | 247 | | B' | 17 | 248 | 249 | ### `solve.js` 250 | 251 | `solve.js` implements [Herbert Kociemba's two-phase algorithm](http://kociemba.org/cube.htm) 252 | for solving the cube quickly in nearly optimal number of moves. 253 | The algorithm is a port of his simple Java implementation without symmetry reductions. 254 | 255 | For the algorithm to work, a computationally intensive precalculation step is required. 256 | Precalculation results in a set of lookup tables that guide the heuristic tree search. 257 | Precalculation takes 4-5 seconds on a typical modern computer, but migh take longer on older machines. 258 | 259 | After precalculation is done, solving a cube with at most 22 moves typically takes 0.01-0.4 seconds, but may take up to 1.5-2 seconds. 260 | Again, these figures apply for a modern computer, and might be bigger on older machines. 261 | 262 | The maximum search depth (numer of moves) can be configured. 263 | The deeper search is allowed, the quicker the solving is. 264 | There's usually no reason to change the default of 22 moves. 265 | 266 | #### Class methods 267 | 268 | ##### `Cube.initSolver()` 269 | 270 | Perform the precalculation step described above. This must be called before calling `solve()`. 271 | 272 | ##### `Cube.scramble()` 273 | 274 | Generate a random scramble by taking a random cube state, solving it, and returning the inverse of the solving algorithm. 275 | By applying this algorithm to a cube, you end up to the random state. 276 | 277 | #### Instance methods 278 | 279 | ##### `solve([maxDepth])` 280 | 281 | Return an algorithm that solves the cube as a string. `maxDepth` is 282 | the maximum number of moves in the solution, and defaults to 22. 283 | 284 | 285 | ## License 286 | 287 | **cube.js** is licensed under the [MIT License](http://opensource.org/licenses/MIT). 288 | 289 | ## History 290 | 291 | **cube.js** was created by [Petri Lehtinen](http://www.digip.org/about/) aka [akheron](https://github.com/akheron). 292 | Thanks to him. 293 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /docs/css/base.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Lora:400,700); 2 | 3 | body { 4 | font-family: 'Lora', 'Georgia', serif; 5 | font-size: 17px; 6 | line-height: 150%; 7 | color: #333; 8 | } 9 | 10 | #container { 11 | margin: 0 auto; 12 | padding: 20px; 13 | max-width: 760px; 14 | background-color: #fff; 15 | } 16 | 17 | h1 { font-size: 28px; padding-bottom: 4px; } 18 | h2 { font-size: 21px; } 19 | h3 { font-size: 17px; } 20 | 21 | h1, h2, h3 { 22 | margin-bottom: 14px; 23 | border-bottom: 1px dashed #ddd; 24 | } 25 | 26 | a { 27 | color: #446; 28 | } 29 | 30 | section { 31 | margin-bottom: 20px; 32 | } 33 | 34 | p, dl { 35 | margin-bottom: 13px; 36 | } 37 | 38 | blockquote { 39 | margin: 13px 26px; 40 | } 41 | 42 | ul { 43 | margin: 0 0 13px 40px; 44 | list-style-type: square; 45 | } 46 | 47 | code, pre { 48 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', 'Monaco', 'Courier New', monospace; 49 | font-size: 14px; 50 | } 51 | 52 | pre { 53 | line-height: 130%; 54 | } 55 | 56 | .pygments, pre.plain { 57 | margin: 13px 25px; 58 | padding: 5px; 59 | border: 1px solid #ddd; 60 | } 61 | 62 | .clear { 63 | clear: both; 64 | } 65 | 66 | table { 67 | margin: 13px 25px; 68 | border: 1px solid #ddd; 69 | } 70 | 71 | table th, table td { 72 | padding: 2px 8px; 73 | text-align: left; 74 | } 75 | 76 | dt { 77 | font-weight: bold; 78 | } 79 | 80 | dd { 81 | margin-left: 26px; 82 | margin-bottom: 8px; 83 | } 84 | -------------------------------------------------------------------------------- /docs/css/html5reset-1.6.css: -------------------------------------------------------------------------------- 1 | /* 2 | html5doctor.com Reset Stylesheet 3 | v1.6 4 | Last Updated: 2010-08-18 5 | Author: Richard Clark - http://richclarkdesign.com 6 | Twitter: @rich_clark 7 | */ 8 | 9 | html, body, div, span, object, iframe, 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 11 | abbr, address, cite, code, 12 | del, dfn, em, img, ins, kbd, q, samp, 13 | small, strong, sub, sup, var, 14 | b, i, 15 | dl, dt, dd, ol, ul, li, 16 | fieldset, form, label, legend, 17 | table, caption, tbody, tfoot, thead, tr, th, td, 18 | article, aside, canvas, details, figcaption, figure, 19 | footer, header, hgroup, menu, nav, section, summary, 20 | time, mark, audio, video { 21 | margin:0; 22 | padding:0; 23 | border:0; 24 | outline:0; 25 | font-size:100%; 26 | vertical-align:baseline; 27 | background:transparent; 28 | } 29 | 30 | body { 31 | line-height:1; 32 | } 33 | 34 | article,aside,details,figcaption,figure, 35 | footer,header,hgroup,menu,nav,section { 36 | display:block; 37 | } 38 | 39 | nav ul { 40 | list-style:none; 41 | } 42 | 43 | blockquote, q { 44 | quotes:none; 45 | } 46 | 47 | blockquote:before, blockquote:after, 48 | q:before, q:after { 49 | content:''; 50 | content:none; 51 | } 52 | 53 | a { 54 | margin:0; 55 | padding:0; 56 | font-size:100%; 57 | vertical-align:baseline; 58 | background:transparent; 59 | } 60 | 61 | /* change colours to suit your needs */ 62 | ins { 63 | background-color:#ff9; 64 | color:#000; 65 | text-decoration:none; 66 | } 67 | 68 | /* change colours to suit your needs */ 69 | mark { 70 | background-color:#ff9; 71 | color:#000; 72 | font-style:italic; 73 | font-weight:bold; 74 | } 75 | 76 | del { 77 | text-decoration: line-through; 78 | } 79 | 80 | abbr[title], dfn[title] { 81 | border-bottom:1px dotted inherit; 82 | cursor:help; 83 | } 84 | 85 | table { 86 | border-collapse:collapse; 87 | border-spacing:0; 88 | } 89 | 90 | /* change border colour to suit your needs */ 91 | hr { 92 | display:block; 93 | height:1px; 94 | border:0; 95 | border-top:1px solid #cccccc; 96 | margin:1em 0; 97 | padding:0; 98 | } 99 | 100 | input, select { 101 | vertical-align:middle; 102 | } 103 | -------------------------------------------------------------------------------- /docs/css/pygments.css: -------------------------------------------------------------------------------- 1 | /* Style "trac" from Pygments 1.4 */ 2 | .pygments .hll { background-color: #ffffcc } 3 | .pygments { background: #ffffff; } 4 | .pygments .c { color: #999988; font-style: italic } /* Comment */ 5 | .pygments .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 6 | .pygments .k { font-weight: bold } /* Keyword */ 7 | .pygments .o { font-weight: bold } /* Operator */ 8 | .pygments .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 9 | .pygments .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 10 | .pygments .c1 { color: #999988; font-style: italic } /* Comment.Single */ 11 | .pygments .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 12 | .pygments .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 13 | .pygments .ge { font-style: italic } /* Generic.Emph */ 14 | .pygments .gr { color: #aa0000 } /* Generic.Error */ 15 | .pygments .gh { color: #999999 } /* Generic.Heading */ 16 | .pygments .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 17 | .pygments .go { color: #888888 } /* Generic.Output */ 18 | .pygments .gp { color: #555555 } /* Generic.Prompt */ 19 | .pygments .gs { font-weight: bold } /* Generic.Strong */ 20 | .pygments .gu { color: #aaaaaa } /* Generic.Subheading */ 21 | .pygments .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .pygments .kc { font-weight: bold } /* Keyword.Constant */ 23 | .pygments .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .pygments .kn { font-weight: bold } /* Keyword.Namespace */ 25 | .pygments .kp { font-weight: bold } /* Keyword.Pseudo */ 26 | .pygments .kr { font-weight: bold } /* Keyword.Reserved */ 27 | .pygments .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 28 | .pygments .m { color: #009999 } /* Literal.Number */ 29 | .pygments .s { color: #bb8844 } /* Literal.String */ 30 | .pygments .na { color: #008080 } /* Name.Attribute */ 31 | .pygments .nb { color: #999999 } /* Name.Builtin */ 32 | .pygments .nc { color: #445588; font-weight: bold } /* Name.Class */ 33 | .pygments .no { color: #008080 } /* Name.Constant */ 34 | .pygments .ni { color: #800080 } /* Name.Entity */ 35 | .pygments .ne { color: #990000; font-weight: bold } /* Name.Exception */ 36 | .pygments .nf { color: #990000; font-weight: bold } /* Name.Function */ 37 | .pygments .nn { color: #555555 } /* Name.Namespace */ 38 | .pygments .nt { color: #000080 } /* Name.Tag */ 39 | .pygments .nv { color: #008080 } /* Name.Variable */ 40 | .pygments .ow { font-weight: bold } /* Operator.Word */ 41 | .pygments .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .pygments .mf { color: #009999 } /* Literal.Number.Float */ 43 | .pygments .mh { color: #009999 } /* Literal.Number.Hex */ 44 | .pygments .mi { color: #009999 } /* Literal.Number.Integer */ 45 | .pygments .mo { color: #009999 } /* Literal.Number.Oct */ 46 | .pygments .sb { color: #bb8844 } /* Literal.String.Backtick */ 47 | .pygments .sc { color: #bb8844 } /* Literal.String.Char */ 48 | .pygments .sd { color: #bb8844 } /* Literal.String.Doc */ 49 | .pygments .s2 { color: #bb8844 } /* Literal.String.Double */ 50 | .pygments .se { color: #bb8844 } /* Literal.String.Escape */ 51 | .pygments .sh { color: #bb8844 } /* Literal.String.Heredoc */ 52 | .pygments .si { color: #bb8844 } /* Literal.String.Interpol */ 53 | .pygments .sx { color: #bb8844 } /* Literal.String.Other */ 54 | .pygments .sr { color: #808000 } /* Literal.String.Regex */ 55 | .pygments .s1 { color: #bb8844 } /* Literal.String.Single */ 56 | .pygments .ss { color: #bb8844 } /* Literal.String.Symbol */ 57 | .pygments .bp { color: #999999 } /* Name.Builtin.Pseudo */ 58 | .pygments .vc { color: #008080 } /* Name.Variable.Class */ 59 | .pygments .vg { color: #008080 } /* Name.Variable.Global */ 60 | .pygments .vi { color: #008080 } /* Name.Variable.Instance */ 61 | .pygments .il { color: #009999 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cube.js Demo - standalone page 8 | 9 | 14 | 15 | 16 | 17 |

18 |

19 |
20 | 21 |

22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/demo.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | let progressHandle; 5 | 6 | function progress() { 7 | // Add a dot each second 8 | $('#status').text(function(index, text) { 9 | return `${text} .`; 10 | }); 11 | } 12 | 13 | function initialized() { 14 | // Precomputing finished, stop adding dots 15 | clearInterval(progressHandle); 16 | 17 | // Show the duration of initialization 18 | let end = new Date; 19 | let duration = (end - start) / 1000; 20 | $('#status').text(`Initialization done in ${duration} seconds.`); 21 | 22 | // Show the scrambler 23 | $('#randomstate').css('visibility', 'visible'); 24 | $('#randomstate button').on('click', generateScramble); 25 | } 26 | 27 | function generateScramble() { 28 | // Hide the initialization status on first scramble 29 | $('#status').hide(); 30 | 31 | // Generate a scramble 32 | Cube.asyncScramble(function(alg) { 33 | let safeAlgo = alg.replace(/\s+/g, ''); // remove spaces 34 | let url = `http://cube.rider.biz/visualcube.php?fmt=svg&size=150&pzl=3&alg=${safeAlgo}`; 35 | $('#randomstate .result').html(`${alg}
`); 36 | }); 37 | } 38 | 39 | let start; 40 | 41 | $(function() { 42 | $('#status').text('Initializing'); 43 | 44 | // Start measuring time 45 | start = new Date; 46 | 47 | // Start adding dots 48 | progressHandle = setInterval(progress, 1000); 49 | 50 | // Start precomputing 51 | Cube.asyncInit('./lib/worker.js', initialized); 52 | }); 53 | })(); 54 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Cube.js Demo 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | 28 |
29 | 30 |

cube.js random-state scrambler demo

31 |

32 | This is a simple random state scrambler created with cube.js. After the initialization is complete, click on the "generate random scramble" button to generate a random-state scramble. 33 |

34 |

35 | Graphics are generated using Conrad Rider's VisualCube. 36 |

37 |

38 | The demo scrambler and its code are presented below. Click here for a standalone page. 39 |

40 | 41 |

The scrambler

42 |

43 |

44 |
45 | 46 |

47 | 48 |

The code

49 |

50 | demo.html: 51 |

52 |
53 |
 54 |         <!DOCTYPE html>
 55 |         <head>
 56 |           <style>
 57 |             #randomstate { visibility: hidden; }
 58 |           </style>
 59 |         </head>
 60 | 
 61 |         <body>
 62 |           <p id="status"></p>
 63 |           <p id="randomstate">
 64 |             <button>Generate random scramble</button><br>
 65 |             <span class="result"></span>
 66 |           </p>
 67 | 
 68 |           <script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
 69 |           <script src="/cubejs/lib/cube.js"></script>
 70 |           <script src="/cubejs/lib/async.js"></script>
 71 |           <script src="demo.js"></script>
 72 |         </body>
 73 |       
74 |
75 |

76 | demo.js: 77 |

78 |
79 |
 80 |         (function() {
 81 |         var start, progressHandle;
 82 | 
 83 |         var progress = function() {
 84 |             // Add a dot each second
 85 |             $('#status').text(function(index, text) { return text + '.'; });
 86 |         };
 87 | 
 88 |         var initialized = function() {
 89 |             // Precomputing finished, stop adding dots
 90 |             clearInterval(progressHandle);
 91 | 
 92 |             // Show the duration of initialization
 93 |             var end = new Date,
 94 |                 duration = (end - start) / 1000;
 95 |             $('#status').text('Initialization done in ' + duration + ' seconds.');
 96 | 
 97 |             // Show the scrambler
 98 |             $('#randomstate').css('visibility', 'visible');
 99 |             $('#randomstate button').on('click', generateScramble);
100 |         };
101 | 
102 |         var generateScramble = function() {
103 |             // Hide the initialization status on first scramble
104 |             $('#status').hide();
105 | 
106 |             // Generate a scramble
107 |             Cube.asyncScramble(function(alg) {
108 |                 var s = alg.replace(/\s+/g, ''),  // remove spaces
109 |                     url = "http://cube.rider.biz/visualcube.png?size=150&alg=" + s;
110 |                 $('#randomstate .result').html(alg + "<br><img src=\"" + url + "\">");
111 |             });
112 |         };
113 | 
114 |         $(function() {
115 |             $('#status').text('Initializing');
116 | 
117 |             // Start measuring time
118 |             start = new Date;
119 | 
120 |             // Start adding dots
121 |             progressHandle = setInterval(progress, 1000);
122 | 
123 |             // Start precomputing
124 |             Cube.asyncInit('/cubejs/lib/worker.js', initialized);
125 |         });
126 |         })();
127 |       
128 |
129 | 130 | 131 |
132 | 133 | 134 | 135 | 136 | 137 | 138 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/lib/async.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Cube, Extend, key, value; 3 | 4 | Cube = this.Cube || require('./cube'); 5 | 6 | Extend = { 7 | asyncOK: !!window.Worker, 8 | _asyncSetup: function(workerURI) { 9 | if (this._worker) { 10 | return; 11 | } 12 | this._worker = new window.Worker(workerURI); 13 | this._worker.addEventListener('message', (e) => { 14 | return this._asyncEvent(e); 15 | }); 16 | return this._asyncCallbacks = {}; 17 | }, 18 | _asyncEvent: function(e) { 19 | var callback, callbacks; 20 | callbacks = this._asyncCallbacks[e.data.cmd]; 21 | if (!(callbacks && callbacks.length)) { 22 | return; 23 | } 24 | callback = callbacks[0]; 25 | callbacks.splice(0, 1); 26 | return callback(e.data); 27 | }, 28 | _asyncCallback: function(cmd, callback) { 29 | var base; 30 | (base = this._asyncCallbacks)[cmd] || (base[cmd] = []); 31 | return this._asyncCallbacks[cmd].push(callback); 32 | }, 33 | asyncInit: function(workerURI, callback) { 34 | this._asyncSetup(workerURI); 35 | this._asyncCallback('init', function() { 36 | return callback(); 37 | }); 38 | return this._worker.postMessage({ 39 | cmd: 'init' 40 | }); 41 | }, 42 | _asyncSolve: function(cube, callback) { 43 | this._asyncSetup(); 44 | this._asyncCallback('solve', function(data) { 45 | return callback(data.algorithm); 46 | }); 47 | return this._worker.postMessage({ 48 | cmd: 'solve', 49 | cube: cube.toJSON() 50 | }); 51 | }, 52 | asyncScramble: function(callback) { 53 | this._asyncSetup(); 54 | this._asyncCallback('solve', function(data) { 55 | return callback(Cube.inverse(data.algorithm)); 56 | }); 57 | return this._worker.postMessage({ 58 | cmd: 'solve', 59 | cube: Cube.random().toJSON() 60 | }); 61 | }, 62 | asyncSolve: function(callback) { 63 | return Cube._asyncSolve(this, callback); 64 | } 65 | }; 66 | 67 | for (key in Extend) { 68 | value = Extend[key]; 69 | Cube[key] = value; 70 | } 71 | 72 | }).call(this); 73 | -------------------------------------------------------------------------------- /docs/lib/cube.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Centers 3 | var B, BL, BR, Cube, D, DB, DBL, DF, DFR, DL, DLF, DR, DRB, F, FL, FR, L, R, U, UB, UBR, UF, UFL, UL, ULB, UR, URF, centerColor, centerFacelet, cornerColor, cornerFacelet, edgeColor, edgeFacelet; 4 | 5 | [U, R, F, D, L, B] = [0, 1, 2, 3, 4, 5]; 6 | 7 | // Corners 8 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0, 1, 2, 3, 4, 5, 6, 7]; 9 | 10 | // Edges 11 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 12 | 13 | [centerFacelet, cornerFacelet, edgeFacelet] = (function() { 14 | var _B, _D, _F, _L, _R, _U; 15 | _U = function(x) { 16 | return x - 1; 17 | }; 18 | _R = function(x) { 19 | return _U(9) + x; 20 | }; 21 | _F = function(x) { 22 | return _R(9) + x; 23 | }; 24 | _D = function(x) { 25 | return _F(9) + x; 26 | }; 27 | _L = function(x) { 28 | return _D(9) + x; 29 | }; 30 | _B = function(x) { 31 | return _L(9) + x; 32 | }; 33 | return [ 34 | // Centers 35 | [4, 36 | 13, 37 | 22, 38 | 31, 39 | 40, 40 | 49], 41 | // Corners 42 | [[_U(9), 43 | _R(1), 44 | _F(3)], 45 | [_U(7), 46 | _F(1), 47 | _L(3)], 48 | [_U(1), 49 | _L(1), 50 | _B(3)], 51 | [_U(3), 52 | _B(1), 53 | _R(3)], 54 | [_D(3), 55 | _F(9), 56 | _R(7)], 57 | [_D(1), 58 | _L(9), 59 | _F(7)], 60 | [_D(7), 61 | _B(9), 62 | _L(7)], 63 | [_D(9), 64 | _R(9), 65 | _B(7)]], 66 | // Edges 67 | [[_U(6), 68 | _R(2)], 69 | [_U(8), 70 | _F(2)], 71 | [_U(4), 72 | _L(2)], 73 | [_U(2), 74 | _B(2)], 75 | [_D(6), 76 | _R(8)], 77 | [_D(2), 78 | _F(8)], 79 | [_D(4), 80 | _L(8)], 81 | [_D(8), 82 | _B(8)], 83 | [_F(6), 84 | _R(4)], 85 | [_F(4), 86 | _L(6)], 87 | [_B(6), 88 | _L(4)], 89 | [_B(4), 90 | _R(6)]] 91 | ]; 92 | })(); 93 | 94 | centerColor = ['U', 'R', 'F', 'D', 'L', 'B']; 95 | 96 | cornerColor = [['U', 'R', 'F'], ['U', 'F', 'L'], ['U', 'L', 'B'], ['U', 'B', 'R'], ['D', 'F', 'R'], ['D', 'L', 'F'], ['D', 'B', 'L'], ['D', 'R', 'B']]; 97 | 98 | edgeColor = [['U', 'R'], ['U', 'F'], ['U', 'L'], ['U', 'B'], ['D', 'R'], ['D', 'F'], ['D', 'L'], ['D', 'B'], ['F', 'R'], ['F', 'L'], ['B', 'L'], ['B', 'R']]; 99 | 100 | Cube = (function() { 101 | var faceNames, faceNums, parseAlg; 102 | 103 | class Cube { 104 | constructor(other) { 105 | var x; 106 | if (other != null) { 107 | this.init(other); 108 | } else { 109 | this.identity(); 110 | } 111 | // For moves to avoid allocating new objects each time 112 | this.newCenter = (function() { 113 | var k, results; 114 | results = []; 115 | for (x = k = 0; k <= 5; x = ++k) { 116 | results.push(0); 117 | } 118 | return results; 119 | })(); 120 | this.newCp = (function() { 121 | var k, results; 122 | results = []; 123 | for (x = k = 0; k <= 7; x = ++k) { 124 | results.push(0); 125 | } 126 | return results; 127 | })(); 128 | this.newEp = (function() { 129 | var k, results; 130 | results = []; 131 | for (x = k = 0; k <= 11; x = ++k) { 132 | results.push(0); 133 | } 134 | return results; 135 | })(); 136 | this.newCo = (function() { 137 | var k, results; 138 | results = []; 139 | for (x = k = 0; k <= 7; x = ++k) { 140 | results.push(0); 141 | } 142 | return results; 143 | })(); 144 | this.newEo = (function() { 145 | var k, results; 146 | results = []; 147 | for (x = k = 0; k <= 11; x = ++k) { 148 | results.push(0); 149 | } 150 | return results; 151 | })(); 152 | } 153 | 154 | init(state) { 155 | this.center = state.center.slice(0); 156 | this.co = state.co.slice(0); 157 | this.ep = state.ep.slice(0); 158 | this.cp = state.cp.slice(0); 159 | return this.eo = state.eo.slice(0); 160 | } 161 | 162 | identity() { 163 | var x; 164 | // Initialize to the identity cube 165 | this.center = [0, 1, 2, 3, 4, 5]; 166 | this.cp = [0, 1, 2, 3, 4, 5, 6, 7]; 167 | this.co = (function() { 168 | var k, results; 169 | results = []; 170 | for (x = k = 0; k <= 7; x = ++k) { 171 | results.push(0); 172 | } 173 | return results; 174 | })(); 175 | this.ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 176 | return this.eo = (function() { 177 | var k, results; 178 | results = []; 179 | for (x = k = 0; k <= 11; x = ++k) { 180 | results.push(0); 181 | } 182 | return results; 183 | })(); 184 | } 185 | 186 | toJSON() { 187 | return { 188 | center: this.center, 189 | cp: this.cp, 190 | co: this.co, 191 | ep: this.ep, 192 | eo: this.eo 193 | }; 194 | } 195 | 196 | asString() { 197 | var corner, edge, i, k, l, m, n, o, ori, p, result; 198 | result = []; 199 | for (i = k = 0; k <= 5; i = ++k) { 200 | result[9 * i + 4] = centerColor[this.center[i]]; 201 | } 202 | for (i = l = 0; l <= 7; i = ++l) { 203 | corner = this.cp[i]; 204 | ori = this.co[i]; 205 | for (n = m = 0; m <= 2; n = ++m) { 206 | result[cornerFacelet[i][(n + ori) % 3]] = cornerColor[corner][n]; 207 | } 208 | } 209 | for (i = o = 0; o <= 11; i = ++o) { 210 | edge = this.ep[i]; 211 | ori = this.eo[i]; 212 | for (n = p = 0; p <= 1; n = ++p) { 213 | result[edgeFacelet[i][(n + ori) % 2]] = edgeColor[edge][n]; 214 | } 215 | } 216 | return result.join(''); 217 | } 218 | 219 | static fromString(str) { 220 | var col1, col2, cube, i, j, k, l, m, o, ori, p, q, r, ref; 221 | cube = new Cube(); 222 | for (i = k = 0; k <= 5; i = ++k) { 223 | for (j = l = 0; l <= 5; j = ++l) { 224 | if (str[9 * i + 4] === centerColor[j]) { 225 | cube.center[i] = j; 226 | } 227 | } 228 | } 229 | for (i = m = 0; m <= 7; i = ++m) { 230 | for (ori = o = 0; o <= 2; ori = ++o) { 231 | if ((ref = str[cornerFacelet[i][ori]]) === 'U' || ref === 'D') { 232 | break; 233 | } 234 | } 235 | col1 = str[cornerFacelet[i][(ori + 1) % 3]]; 236 | col2 = str[cornerFacelet[i][(ori + 2) % 3]]; 237 | for (j = p = 0; p <= 7; j = ++p) { 238 | if (col1 === cornerColor[j][1] && col2 === cornerColor[j][2]) { 239 | cube.cp[i] = j; 240 | cube.co[i] = ori % 3; 241 | } 242 | } 243 | } 244 | for (i = q = 0; q <= 11; i = ++q) { 245 | for (j = r = 0; r <= 11; j = ++r) { 246 | if (str[edgeFacelet[i][0]] === edgeColor[j][0] && str[edgeFacelet[i][1]] === edgeColor[j][1]) { 247 | cube.ep[i] = j; 248 | cube.eo[i] = 0; 249 | break; 250 | } 251 | if (str[edgeFacelet[i][0]] === edgeColor[j][1] && str[edgeFacelet[i][1]] === edgeColor[j][0]) { 252 | cube.ep[i] = j; 253 | cube.eo[i] = 1; 254 | break; 255 | } 256 | } 257 | } 258 | return cube; 259 | } 260 | 261 | clone() { 262 | return new Cube(this.toJSON()); 263 | } 264 | 265 | // A class method returning a new random cube 266 | static random() { 267 | return new Cube().randomize(); 268 | } 269 | 270 | isSolved() { 271 | var c, cent, clone, e, k, l, m; 272 | clone = this.clone(); 273 | clone.move(clone.upright()); 274 | for (cent = k = 0; k <= 5; cent = ++k) { 275 | if (clone.center[cent] !== cent) { 276 | return false; 277 | } 278 | } 279 | for (c = l = 0; l <= 7; c = ++l) { 280 | if (clone.cp[c] !== c) { 281 | return false; 282 | } 283 | if (clone.co[c] !== 0) { 284 | return false; 285 | } 286 | } 287 | for (e = m = 0; m <= 11; e = ++m) { 288 | if (clone.ep[e] !== e) { 289 | return false; 290 | } 291 | if (clone.eo[e] !== 0) { 292 | return false; 293 | } 294 | } 295 | return true; 296 | } 297 | 298 | // Multiply this Cube with another Cube, restricted to centers. 299 | centerMultiply(other) { 300 | var from, k, to; 301 | for (to = k = 0; k <= 5; to = ++k) { 302 | from = other.center[to]; 303 | this.newCenter[to] = this.center[from]; 304 | } 305 | [this.center, this.newCenter] = [this.newCenter, this.center]; 306 | return this; 307 | } 308 | 309 | // Multiply this Cube with another Cube, restricted to corners. 310 | cornerMultiply(other) { 311 | var from, k, to; 312 | for (to = k = 0; k <= 7; to = ++k) { 313 | from = other.cp[to]; 314 | this.newCp[to] = this.cp[from]; 315 | this.newCo[to] = (this.co[from] + other.co[to]) % 3; 316 | } 317 | [this.cp, this.newCp] = [this.newCp, this.cp]; 318 | [this.co, this.newCo] = [this.newCo, this.co]; 319 | return this; 320 | } 321 | 322 | // Multiply this Cube with another Cube, restricted to edges 323 | edgeMultiply(other) { 324 | var from, k, to; 325 | for (to = k = 0; k <= 11; to = ++k) { 326 | from = other.ep[to]; 327 | this.newEp[to] = this.ep[from]; 328 | this.newEo[to] = (this.eo[from] + other.eo[to]) % 2; 329 | } 330 | [this.ep, this.newEp] = [this.newEp, this.ep]; 331 | [this.eo, this.newEo] = [this.newEo, this.eo]; 332 | return this; 333 | } 334 | 335 | // Multiply this cube with another Cube 336 | multiply(other) { 337 | this.centerMultiply(other); 338 | this.cornerMultiply(other); 339 | this.edgeMultiply(other); 340 | return this; 341 | } 342 | 343 | move(arg) { 344 | var face, k, l, len, move, power, ref, ref1, x; 345 | ref = parseAlg(arg); 346 | for (k = 0, len = ref.length; k < len; k++) { 347 | move = ref[k]; 348 | face = move / 3 | 0; 349 | power = move % 3; 350 | for (x = l = 0, ref1 = power; (0 <= ref1 ? l <= ref1 : l >= ref1); x = 0 <= ref1 ? ++l : --l) { 351 | this.multiply(Cube.moves[face]); 352 | } 353 | } 354 | return this; 355 | } 356 | 357 | upright() { 358 | var clone, i, j, k, l, result; 359 | clone = this.clone(); 360 | result = []; 361 | for (i = k = 0; k <= 5; i = ++k) { 362 | if (clone.center[i] === F) { 363 | break; 364 | } 365 | } 366 | switch (i) { 367 | case D: 368 | result.push("x"); 369 | break; 370 | case U: 371 | result.push("x'"); 372 | break; 373 | case B: 374 | result.push("x2"); 375 | break; 376 | case R: 377 | result.push("y"); 378 | break; 379 | case L: 380 | result.push("y'"); 381 | } 382 | if (result.length) { 383 | clone.move(result[0]); 384 | } 385 | for (j = l = 0; l <= 5; j = ++l) { 386 | if (clone.center[j] === U) { 387 | break; 388 | } 389 | } 390 | switch (j) { 391 | case L: 392 | result.push("z"); 393 | break; 394 | case R: 395 | result.push("z'"); 396 | break; 397 | case D: 398 | result.push("z2"); 399 | } 400 | return result.join(' '); 401 | } 402 | 403 | static inverse(arg) { 404 | var face, k, len, move, power, result, str; 405 | result = (function() { 406 | var k, len, ref, results; 407 | ref = parseAlg(arg); 408 | results = []; 409 | for (k = 0, len = ref.length; k < len; k++) { 410 | move = ref[k]; 411 | face = move / 3 | 0; 412 | power = move % 3; 413 | results.push(face * 3 + -(power - 1) + 1); 414 | } 415 | return results; 416 | })(); 417 | result.reverse(); 418 | if (typeof arg === 'string') { 419 | str = ''; 420 | for (k = 0, len = result.length; k < len; k++) { 421 | move = result[k]; 422 | face = move / 3 | 0; 423 | power = move % 3; 424 | str += faceNames[face]; 425 | if (power === 1) { 426 | str += '2'; 427 | } else if (power === 2) { 428 | str += "'"; 429 | } 430 | str += ' '; 431 | } 432 | return str.substring(0, str.length - 1); 433 | } else if (arg.length != null) { 434 | return result; 435 | } else { 436 | return result[0]; 437 | } 438 | } 439 | 440 | }; 441 | 442 | Cube.prototype.randomize = (function() { 443 | var arePermutationsValid, generateValidRandomOrientation, generateValidRandomPermutation, getNumSwaps, isOrientationValid, randint, randomizeOrientation, result, shuffle; 444 | randint = function(min, max) { 445 | return min + Math.floor(Math.random() * (max - min + 1)); 446 | }; 447 | // Fisher-Yates shuffle adapted from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array 448 | shuffle = function(array) { 449 | var currentIndex, randomIndex, temporaryValue; 450 | currentIndex = array.length; 451 | // While there remain elements to shuffle... 452 | while (currentIndex !== 0) { 453 | // Pick a remaining element... 454 | randomIndex = randint(0, currentIndex - 1); 455 | currentIndex -= 1; 456 | // And swap it with the current element. 457 | temporaryValue = array[currentIndex]; 458 | [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; 459 | } 460 | }; 461 | getNumSwaps = function(arr) { 462 | var cur, cycleLength, i, k, numSwaps, ref, seen, x; 463 | numSwaps = 0; 464 | seen = (function() { 465 | var k, ref, results; 466 | results = []; 467 | for (x = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); x = 0 <= ref ? ++k : --k) { 468 | results.push(false); 469 | } 470 | return results; 471 | })(); 472 | while (true) { 473 | // We compute the cycle decomposition 474 | cur = -1; 475 | for (i = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); i = 0 <= ref ? ++k : --k) { 476 | if (!seen[i]) { 477 | cur = i; 478 | break; 479 | } 480 | } 481 | if (cur === -1) { 482 | break; 483 | } 484 | cycleLength = 0; 485 | while (!seen[cur]) { 486 | seen[cur] = true; 487 | cycleLength++; 488 | cur = arr[cur]; 489 | } 490 | // A cycle is equivalent to cycleLength + 1 swaps 491 | numSwaps += cycleLength + 1; 492 | } 493 | return numSwaps; 494 | }; 495 | arePermutationsValid = function(cp, ep) { 496 | var numSwaps; 497 | numSwaps = getNumSwaps(ep) + getNumSwaps(cp); 498 | return numSwaps % 2 === 0; 499 | }; 500 | generateValidRandomPermutation = function(cp, ep) { 501 | // Each shuffle only takes around 12 operations and there's a 50% 502 | // chance of a valid permutation so it'll finish in very good time 503 | shuffle(ep); 504 | shuffle(cp); 505 | while (!arePermutationsValid(cp, ep)) { 506 | shuffle(ep); 507 | shuffle(cp); 508 | } 509 | }; 510 | randomizeOrientation = function(arr, numOrientations) { 511 | var i, k, ori, ref; 512 | ori = 0; 513 | for (i = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); i = 0 <= ref ? ++k : --k) { 514 | ori += (arr[i] = randint(0, numOrientations - 1)); 515 | } 516 | }; 517 | isOrientationValid = function(arr, numOrientations) { 518 | return arr.reduce(function(a, b) { 519 | return a + b; 520 | }) % numOrientations === 0; 521 | }; 522 | generateValidRandomOrientation = function(co, eo) { 523 | // There is a 1/2 and 1/3 probably respectively of each of these 524 | // succeeding so the probability of them running 10 times before 525 | // success is already only 1% and only gets exponentially lower 526 | // and each generation is only in the 10s of operations which is nothing 527 | randomizeOrientation(co, 3); 528 | while (!isOrientationValid(co, 3)) { 529 | randomizeOrientation(co, 3); 530 | } 531 | randomizeOrientation(eo, 2); 532 | while (!isOrientationValid(eo, 2)) { 533 | randomizeOrientation(eo, 2); 534 | } 535 | }; 536 | result = function() { 537 | generateValidRandomPermutation(this.cp, this.ep); 538 | generateValidRandomOrientation(this.co, this.eo); 539 | return this; 540 | }; 541 | return result; 542 | })(); 543 | 544 | Cube.moves = [ 545 | { 546 | // U 547 | center: [0, 1, 2, 3, 4, 5], 548 | cp: [UBR, 549 | URF, 550 | UFL, 551 | ULB, 552 | DFR, 553 | DLF, 554 | DBL, 555 | DRB], 556 | co: [0, 557 | 0, 558 | 0, 559 | 0, 560 | 0, 561 | 0, 562 | 0, 563 | 0], 564 | ep: [UB, 565 | UR, 566 | UF, 567 | UL, 568 | DR, 569 | DF, 570 | DL, 571 | DB, 572 | FR, 573 | FL, 574 | BL, 575 | BR], 576 | eo: [0, 577 | 0, 578 | 0, 579 | 0, 580 | 0, 581 | 0, 582 | 0, 583 | 0, 584 | 0, 585 | 0, 586 | 0, 587 | 0] 588 | }, 589 | { 590 | // R 591 | center: [0, 1, 2, 3, 4, 5], 592 | cp: [DFR, 593 | UFL, 594 | ULB, 595 | URF, 596 | DRB, 597 | DLF, 598 | DBL, 599 | UBR], 600 | co: [2, 601 | 0, 602 | 0, 603 | 1, 604 | 1, 605 | 0, 606 | 0, 607 | 2], 608 | ep: [FR, 609 | UF, 610 | UL, 611 | UB, 612 | BR, 613 | DF, 614 | DL, 615 | DB, 616 | DR, 617 | FL, 618 | BL, 619 | UR], 620 | eo: [0, 621 | 0, 622 | 0, 623 | 0, 624 | 0, 625 | 0, 626 | 0, 627 | 0, 628 | 0, 629 | 0, 630 | 0, 631 | 0] 632 | }, 633 | { 634 | // F 635 | center: [0, 1, 2, 3, 4, 5], 636 | cp: [UFL, 637 | DLF, 638 | ULB, 639 | UBR, 640 | URF, 641 | DFR, 642 | DBL, 643 | DRB], 644 | co: [1, 645 | 2, 646 | 0, 647 | 0, 648 | 2, 649 | 1, 650 | 0, 651 | 0], 652 | ep: [UR, 653 | FL, 654 | UL, 655 | UB, 656 | DR, 657 | FR, 658 | DL, 659 | DB, 660 | UF, 661 | DF, 662 | BL, 663 | BR], 664 | eo: [0, 665 | 1, 666 | 0, 667 | 0, 668 | 0, 669 | 1, 670 | 0, 671 | 0, 672 | 1, 673 | 1, 674 | 0, 675 | 0] 676 | }, 677 | { 678 | // D 679 | center: [0, 1, 2, 3, 4, 5], 680 | cp: [URF, 681 | UFL, 682 | ULB, 683 | UBR, 684 | DLF, 685 | DBL, 686 | DRB, 687 | DFR], 688 | co: [0, 689 | 0, 690 | 0, 691 | 0, 692 | 0, 693 | 0, 694 | 0, 695 | 0], 696 | ep: [UR, 697 | UF, 698 | UL, 699 | UB, 700 | DF, 701 | DL, 702 | DB, 703 | DR, 704 | FR, 705 | FL, 706 | BL, 707 | BR], 708 | eo: [0, 709 | 0, 710 | 0, 711 | 0, 712 | 0, 713 | 0, 714 | 0, 715 | 0, 716 | 0, 717 | 0, 718 | 0, 719 | 0] 720 | }, 721 | { 722 | // L 723 | center: [0, 1, 2, 3, 4, 5], 724 | cp: [URF, 725 | ULB, 726 | DBL, 727 | UBR, 728 | DFR, 729 | UFL, 730 | DLF, 731 | DRB], 732 | co: [0, 733 | 1, 734 | 2, 735 | 0, 736 | 0, 737 | 2, 738 | 1, 739 | 0], 740 | ep: [UR, 741 | UF, 742 | BL, 743 | UB, 744 | DR, 745 | DF, 746 | FL, 747 | DB, 748 | FR, 749 | UL, 750 | DL, 751 | BR], 752 | eo: [0, 753 | 0, 754 | 0, 755 | 0, 756 | 0, 757 | 0, 758 | 0, 759 | 0, 760 | 0, 761 | 0, 762 | 0, 763 | 0] 764 | }, 765 | { 766 | // B 767 | center: [0, 1, 2, 3, 4, 5], 768 | cp: [URF, 769 | UFL, 770 | UBR, 771 | DRB, 772 | DFR, 773 | DLF, 774 | ULB, 775 | DBL], 776 | co: [0, 777 | 0, 778 | 1, 779 | 2, 780 | 0, 781 | 0, 782 | 2, 783 | 1], 784 | ep: [UR, 785 | UF, 786 | UL, 787 | BR, 788 | DR, 789 | DF, 790 | DL, 791 | BL, 792 | FR, 793 | FL, 794 | UB, 795 | DB], 796 | eo: [0, 797 | 0, 798 | 0, 799 | 1, 800 | 0, 801 | 0, 802 | 0, 803 | 1, 804 | 0, 805 | 0, 806 | 1, 807 | 1] 808 | }, 809 | { 810 | // E 811 | center: [U, 812 | F, 813 | L, 814 | D, 815 | B, 816 | R], 817 | cp: [URF, 818 | UFL, 819 | ULB, 820 | UBR, 821 | DFR, 822 | DLF, 823 | DBL, 824 | DRB], 825 | co: [0, 826 | 0, 827 | 0, 828 | 0, 829 | 0, 830 | 0, 831 | 0, 832 | 0], 833 | ep: [UR, 834 | UF, 835 | UL, 836 | UB, 837 | DR, 838 | DF, 839 | DL, 840 | DB, 841 | FL, 842 | BL, 843 | BR, 844 | FR], 845 | eo: [0, 846 | 0, 847 | 0, 848 | 0, 849 | 0, 850 | 0, 851 | 0, 852 | 0, 853 | 1, 854 | 1, 855 | 1, 856 | 1] 857 | }, 858 | { 859 | // M 860 | center: [B, 861 | R, 862 | U, 863 | F, 864 | L, 865 | D], 866 | cp: [URF, 867 | UFL, 868 | ULB, 869 | UBR, 870 | DFR, 871 | DLF, 872 | DBL, 873 | DRB], 874 | co: [0, 875 | 0, 876 | 0, 877 | 0, 878 | 0, 879 | 0, 880 | 0, 881 | 0], 882 | ep: [UR, 883 | UB, 884 | UL, 885 | DB, 886 | DR, 887 | UF, 888 | DL, 889 | DF, 890 | FR, 891 | FL, 892 | BL, 893 | BR], 894 | eo: [0, 895 | 1, 896 | 0, 897 | 1, 898 | 0, 899 | 1, 900 | 0, 901 | 1, 902 | 0, 903 | 0, 904 | 0, 905 | 0] 906 | }, 907 | { 908 | // S 909 | center: [L, 910 | U, 911 | F, 912 | R, 913 | D, 914 | B], 915 | cp: [URF, 916 | UFL, 917 | ULB, 918 | UBR, 919 | DFR, 920 | DLF, 921 | DBL, 922 | DRB], 923 | co: [0, 924 | 0, 925 | 0, 926 | 0, 927 | 0, 928 | 0, 929 | 0, 930 | 0], 931 | ep: [UL, 932 | UF, 933 | DL, 934 | UB, 935 | UR, 936 | DF, 937 | DR, 938 | DB, 939 | FR, 940 | FL, 941 | BL, 942 | BR], 943 | eo: [1, 944 | 0, 945 | 1, 946 | 0, 947 | 1, 948 | 0, 949 | 1, 950 | 0, 951 | 0, 952 | 0, 953 | 0, 954 | 0] 955 | } 956 | ]; 957 | 958 | faceNums = { 959 | U: 0, 960 | R: 1, 961 | F: 2, 962 | D: 3, 963 | L: 4, 964 | B: 5, 965 | E: 6, 966 | M: 7, 967 | S: 8, 968 | x: 9, 969 | y: 10, 970 | z: 11, 971 | u: 12, 972 | r: 13, 973 | f: 14, 974 | d: 15, 975 | l: 16, 976 | b: 17 977 | }; 978 | 979 | faceNames = { 980 | 0: 'U', 981 | 1: 'R', 982 | 2: 'F', 983 | 3: 'D', 984 | 4: 'L', 985 | 5: 'B', 986 | 6: 'E', 987 | 7: 'M', 988 | 8: 'S', 989 | 9: 'x', 990 | 10: 'y', 991 | 11: 'z', 992 | 12: 'u', 993 | 13: 'r', 994 | 14: 'f', 995 | 15: 'd', 996 | 16: 'l', 997 | 17: 'b' 998 | }; 999 | 1000 | parseAlg = function(arg) { 1001 | var k, len, move, part, power, ref, results; 1002 | if (typeof arg === 'string') { 1003 | ref = arg.split(/\s+/); 1004 | // String 1005 | results = []; 1006 | for (k = 0, len = ref.length; k < len; k++) { 1007 | part = ref[k]; 1008 | if (part.length === 0) { 1009 | // First and last can be empty 1010 | continue; 1011 | } 1012 | if (part.length > 2) { 1013 | throw new Error(`Invalid move: ${part}`); 1014 | } 1015 | move = faceNums[part[0]]; 1016 | if (move === void 0) { 1017 | throw new Error(`Invalid move: ${part}`); 1018 | } 1019 | if (part.length === 1) { 1020 | power = 0; 1021 | } else { 1022 | if (part[1] === '2') { 1023 | power = 1; 1024 | } else if (part[1] === "'") { 1025 | power = 2; 1026 | } else { 1027 | throw new Error(`Invalid move: ${part}`); 1028 | } 1029 | } 1030 | results.push(move * 3 + power); 1031 | } 1032 | return results; 1033 | } else if (arg.length != null) { 1034 | // Already an array 1035 | return arg; 1036 | } else { 1037 | // A single move 1038 | return [arg]; 1039 | } 1040 | }; 1041 | 1042 | // x 1043 | Cube.moves.push(new Cube().move("R M' L'").toJSON()); 1044 | 1045 | // y 1046 | Cube.moves.push(new Cube().move("U E' D'").toJSON()); 1047 | 1048 | // z 1049 | Cube.moves.push(new Cube().move("F S B'").toJSON()); 1050 | 1051 | // u 1052 | Cube.moves.push(new Cube().move("U E'").toJSON()); 1053 | 1054 | // r 1055 | Cube.moves.push(new Cube().move("R M'").toJSON()); 1056 | 1057 | // f 1058 | Cube.moves.push(new Cube().move("F S").toJSON()); 1059 | 1060 | // d 1061 | Cube.moves.push(new Cube().move("D E").toJSON()); 1062 | 1063 | // l 1064 | Cube.moves.push(new Cube().move("L M").toJSON()); 1065 | 1066 | // b 1067 | Cube.moves.push(new Cube().move("B S'").toJSON()); 1068 | 1069 | return Cube; 1070 | 1071 | }).call(this); 1072 | 1073 | //# Globals 1074 | if (typeof module !== "undefined" && module !== null) { 1075 | module.exports = Cube; 1076 | } else { 1077 | this.Cube = Cube; 1078 | } 1079 | 1080 | }).call(this); 1081 | -------------------------------------------------------------------------------- /docs/lib/solve.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var B, BL, BR, Cnk, Cube, D, DB, DBL, DF, DFR, DL, DLF, DR, DRB, F, FL, FR, Include, L, N_FLIP, N_FRtoBR, N_PARITY, N_SLICE1, N_SLICE2, N_TWIST, N_UBtoDF, N_URFtoDLF, N_URtoDF, N_URtoUL, R, U, UB, UBR, UF, UFL, UL, ULB, UR, URF, allMoves1, allMoves2, computeMoveTable, computePruningTable, faceNames, faceNums, factorial, key, max, mergeURtoDF, moveTableParams, nextMoves1, nextMoves2, permutationIndex, pruning, pruningTableParams, rotateLeft, rotateRight, value, 3 | indexOf = [].indexOf; 4 | 5 | Cube = this.Cube || require('./cube'); 6 | 7 | // Centers 8 | [U, R, F, D, L, B] = [0, 1, 2, 3, 4, 5]; 9 | 10 | // Corners 11 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0, 1, 2, 3, 4, 5, 6, 7]; 12 | 13 | // Edges 14 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 15 | 16 | //# Helpers 17 | 18 | // n choose k, i.e. the binomial coeffiecient 19 | Cnk = function(n, k) { 20 | var i, j, s; 21 | if (n < k) { 22 | return 0; 23 | } 24 | if (k > n / 2) { 25 | k = n - k; 26 | } 27 | s = 1; 28 | i = n; 29 | j = 1; 30 | while (i !== n - k) { 31 | s *= i; 32 | s /= j; 33 | i--; 34 | j++; 35 | } 36 | return s; 37 | }; 38 | 39 | // n! 40 | factorial = function(n) { 41 | var f, i, m, ref; 42 | f = 1; 43 | for (i = m = 2, ref = n; (2 <= ref ? m <= ref : m >= ref); i = 2 <= ref ? ++m : --m) { 44 | f *= i; 45 | } 46 | return f; 47 | }; 48 | 49 | // Maximum of two values 50 | max = function(a, b) { 51 | if (a > b) { 52 | return a; 53 | } else { 54 | return b; 55 | } 56 | }; 57 | 58 | // Rotate elements between l and r left by one place 59 | rotateLeft = function(array, l, r) { 60 | var i, m, ref, ref1, tmp; 61 | tmp = array[l]; 62 | for (i = m = ref = l, ref1 = r - 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 63 | array[i] = array[i + 1]; 64 | } 65 | return array[r] = tmp; 66 | }; 67 | 68 | // Rotate elements between l and r right by one place 69 | rotateRight = function(array, l, r) { 70 | var i, m, ref, ref1, tmp; 71 | tmp = array[r]; 72 | for (i = m = ref = r, ref1 = l + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 73 | array[i] = array[i - 1]; 74 | } 75 | return array[l] = tmp; 76 | }; 77 | 78 | // Generate a function that computes permutation indices. 79 | 80 | // The permutation index actually encodes two indices: Combination, 81 | // i.e. positions of the cubies start..end (A) and their respective 82 | // permutation (B). The maximum value for B is 83 | 84 | // maxB = (end - start + 1)! 85 | 86 | // and the index is A * maxB + B 87 | permutationIndex = function(context, start, end, fromEnd = false) { 88 | var i, maxAll, maxB, maxOur, our, permName; 89 | maxOur = end - start; 90 | maxB = factorial(maxOur + 1); 91 | if (context === 'corners') { 92 | maxAll = 7; 93 | permName = 'cp'; 94 | } else { 95 | maxAll = 11; 96 | permName = 'ep'; 97 | } 98 | our = (function() { 99 | var m, ref, results; 100 | results = []; 101 | for (i = m = 0, ref = maxOur; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 102 | results.push(0); 103 | } 104 | return results; 105 | })(); 106 | return function(index) { 107 | var a, b, c, j, k, m, o, p, perm, q, ref, ref1, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, t, u, w, x, y, z; 108 | if (index != null) { 109 | for (i = m = 0, ref = maxOur; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 110 | // Reset our to [start..end] 111 | our[i] = i + start; 112 | } 113 | b = index % maxB; // permutation 114 | a = index / maxB | 0; // combination 115 | 116 | // Invalidate all edges 117 | perm = this[permName]; 118 | for (i = o = 0, ref1 = maxAll; (0 <= ref1 ? o <= ref1 : o >= ref1); i = 0 <= ref1 ? ++o : --o) { 119 | perm[i] = -1; 120 | } 121 | // Generate permutation from index b 122 | for (j = p = 1, ref2 = maxOur; (1 <= ref2 ? p <= ref2 : p >= ref2); j = 1 <= ref2 ? ++p : --p) { 123 | k = b % (j + 1); 124 | b = b / (j + 1) | 0; 125 | // TODO: Implement rotateRightBy(our, 0, j, k) 126 | while (k > 0) { 127 | rotateRight(our, 0, j); 128 | k--; 129 | } 130 | } 131 | // Generate combination and set our edges 132 | x = maxOur; 133 | if (fromEnd) { 134 | for (j = q = 0, ref3 = maxAll; (0 <= ref3 ? q <= ref3 : q >= ref3); j = 0 <= ref3 ? ++q : --q) { 135 | c = Cnk(maxAll - j, x + 1); 136 | if (a - c >= 0) { 137 | perm[j] = our[maxOur - x]; 138 | a -= c; 139 | x--; 140 | } 141 | } 142 | } else { 143 | for (j = t = ref4 = maxAll; (ref4 <= 0 ? t <= 0 : t >= 0); j = ref4 <= 0 ? ++t : --t) { 144 | c = Cnk(j, x + 1); 145 | if (a - c >= 0) { 146 | perm[j] = our[x]; 147 | a -= c; 148 | x--; 149 | } 150 | } 151 | } 152 | return this; 153 | } else { 154 | perm = this[permName]; 155 | for (i = u = 0, ref5 = maxOur; (0 <= ref5 ? u <= ref5 : u >= ref5); i = 0 <= ref5 ? ++u : --u) { 156 | our[i] = -1; 157 | } 158 | a = b = x = 0; 159 | // Compute the index a < ((maxAll + 1) choose (maxOur + 1)) and 160 | // the permutation 161 | if (fromEnd) { 162 | for (j = w = ref6 = maxAll; (ref6 <= 0 ? w <= 0 : w >= 0); j = ref6 <= 0 ? ++w : --w) { 163 | if ((start <= (ref7 = perm[j]) && ref7 <= end)) { 164 | a += Cnk(maxAll - j, x + 1); 165 | our[maxOur - x] = perm[j]; 166 | x++; 167 | } 168 | } 169 | } else { 170 | for (j = y = 0, ref8 = maxAll; (0 <= ref8 ? y <= ref8 : y >= ref8); j = 0 <= ref8 ? ++y : --y) { 171 | if ((start <= (ref9 = perm[j]) && ref9 <= end)) { 172 | a += Cnk(j, x + 1); 173 | our[x] = perm[j]; 174 | x++; 175 | } 176 | } 177 | } 178 | // Compute the index b < (maxOur + 1)! for the permutation 179 | for (j = z = ref10 = maxOur; (ref10 <= 0 ? z <= 0 : z >= 0); j = ref10 <= 0 ? ++z : --z) { 180 | k = 0; 181 | while (our[j] !== start + j) { 182 | rotateLeft(our, 0, j); 183 | k++; 184 | } 185 | b = (j + 1) * b + k; 186 | } 187 | return a * maxB + b; 188 | } 189 | }; 190 | }; 191 | 192 | Include = { 193 | // The twist of the 8 corners, 0 <= twist < 3^7. The orientation of 194 | // the DRB corner is fully determined by the orientation of the other 195 | // corners. 196 | twist: function(twist) { 197 | var i, m, o, ori, parity, v; 198 | if (twist != null) { 199 | parity = 0; 200 | for (i = m = 6; m >= 0; i = --m) { 201 | ori = twist % 3; 202 | twist = (twist / 3) | 0; 203 | this.co[i] = ori; 204 | parity += ori; 205 | } 206 | this.co[7] = (3 - parity % 3) % 3; 207 | return this; 208 | } else { 209 | v = 0; 210 | for (i = o = 0; o <= 6; i = ++o) { 211 | v = 3 * v + this.co[i]; 212 | } 213 | return v; 214 | } 215 | }, 216 | // The flip of the 12 edges, 0 <= flip < 2^11. The orientation of the 217 | // BR edge is fully determined by the orientation of the other edges. 218 | flip: function(flip) { 219 | var i, m, o, ori, parity, v; 220 | if (flip != null) { 221 | parity = 0; 222 | for (i = m = 10; m >= 0; i = --m) { 223 | ori = flip % 2; 224 | flip = flip / 2 | 0; 225 | this.eo[i] = ori; 226 | parity += ori; 227 | } 228 | this.eo[11] = (2 - parity % 2) % 2; 229 | return this; 230 | } else { 231 | v = 0; 232 | for (i = o = 0; o <= 10; i = ++o) { 233 | v = 2 * v + this.eo[i]; 234 | } 235 | return v; 236 | } 237 | }, 238 | // Parity of the corner permutation 239 | cornerParity: function() { 240 | var i, j, m, o, ref, ref1, ref2, ref3, s; 241 | s = 0; 242 | for (i = m = ref = DRB, ref1 = URF + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 243 | for (j = o = ref2 = i - 1, ref3 = URF; (ref2 <= ref3 ? o <= ref3 : o >= ref3); j = ref2 <= ref3 ? ++o : --o) { 244 | if (this.cp[j] > this.cp[i]) { 245 | s++; 246 | } 247 | } 248 | } 249 | return s % 2; 250 | }, 251 | // Parity of the edges permutation. Parity of corners and edges are 252 | // the same if the cube is solvable. 253 | edgeParity: function() { 254 | var i, j, m, o, ref, ref1, ref2, ref3, s; 255 | s = 0; 256 | for (i = m = ref = BR, ref1 = UR + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 257 | for (j = o = ref2 = i - 1, ref3 = UR; (ref2 <= ref3 ? o <= ref3 : o >= ref3); j = ref2 <= ref3 ? ++o : --o) { 258 | if (this.ep[j] > this.ep[i]) { 259 | s++; 260 | } 261 | } 262 | } 263 | return s % 2; 264 | }, 265 | // Permutation of the six corners URF, UFL, ULB, UBR, DFR, DLF 266 | URFtoDLF: permutationIndex('corners', URF, DLF), 267 | // Permutation of the three edges UR, UF, UL 268 | URtoUL: permutationIndex('edges', UR, UL), 269 | // Permutation of the three edges UB, DR, DF 270 | UBtoDF: permutationIndex('edges', UB, DF), 271 | // Permutation of the six edges UR, UF, UL, UB, DR, DF 272 | URtoDF: permutationIndex('edges', UR, DF), 273 | // Permutation of the equator slice edges FR, FL, BL and BR 274 | FRtoBR: permutationIndex('edges', FR, BR, true) 275 | }; 276 | 277 | for (key in Include) { 278 | value = Include[key]; 279 | Cube.prototype[key] = value; 280 | } 281 | 282 | computeMoveTable = function(context, coord, size) { 283 | var apply, cube, i, inner, j, k, m, move, o, p, ref, results; 284 | // Loop through all valid values for the coordinate, setting cube's 285 | // state in each iteration. Then apply each of the 18 moves to the 286 | // cube, and compute the resulting coordinate. 287 | apply = context === 'corners' ? 'cornerMultiply' : 'edgeMultiply'; 288 | cube = new Cube(); 289 | results = []; 290 | for (i = m = 0, ref = size - 1; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 291 | cube[coord](i); 292 | inner = []; 293 | for (j = o = 0; o <= 5; j = ++o) { 294 | move = Cube.moves[j]; 295 | for (k = p = 0; p <= 2; k = ++p) { 296 | cube[apply](move); 297 | inner.push(cube[coord]()); 298 | } 299 | // 4th face turn restores the cube 300 | cube[apply](move); 301 | } 302 | results.push(inner); 303 | } 304 | return results; 305 | }; 306 | 307 | // Because we only have the phase 2 URtoDF coordinates, we need to 308 | // merge the URtoUL and UBtoDF coordinates to URtoDF in the beginning 309 | // of phase 2. 310 | mergeURtoDF = (function() { 311 | var a, b; 312 | a = new Cube(); 313 | b = new Cube(); 314 | return function(URtoUL, UBtoDF) { 315 | var i, m; 316 | // Collisions can be found because unset are set to -1 317 | a.URtoUL(URtoUL); 318 | b.UBtoDF(UBtoDF); 319 | for (i = m = 0; m <= 7; i = ++m) { 320 | if (a.ep[i] !== -1) { 321 | if (b.ep[i] !== -1) { 322 | return -1; // collision 323 | } else { 324 | b.ep[i] = a.ep[i]; 325 | } 326 | } 327 | } 328 | return b.URtoDF(); 329 | }; 330 | })(); 331 | 332 | N_TWIST = 2187; // 3^7 corner orientations 333 | 334 | N_FLIP = 2048; // 2^11 possible edge flips 335 | 336 | N_PARITY = 2; // 2 possible parities 337 | 338 | N_FRtoBR = 11880; // 12!/(12-4)! permutations of FR..BR edges 339 | 340 | N_SLICE1 = 495; // (12 choose 4) possible positions of FR..BR edges 341 | 342 | N_SLICE2 = 24; // 4! permutations of FR..BR edges in phase 2 343 | 344 | N_URFtoDLF = 20160; // 8!/(8-6)! permutations of URF..DLF corners 345 | 346 | 347 | // The URtoDF move table is only computed for phase 2 because the full 348 | // table would have >650000 entries 349 | N_URtoDF = 20160; // 8!/(8-6)! permutation of UR..DF edges in phase 2 350 | 351 | N_URtoUL = 1320; // 12!/(12-3)! permutations of UR..UL edges 352 | 353 | N_UBtoDF = 1320; // 12!/(12-3)! permutations of UB..DF edges 354 | 355 | 356 | // The move table for parity is so small that it's included here 357 | Cube.moveTables = { 358 | parity: [[1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]], 359 | twist: null, 360 | flip: null, 361 | FRtoBR: null, 362 | URFtoDLF: null, 363 | URtoDF: null, 364 | URtoUL: null, 365 | UBtoDF: null, 366 | mergeURtoDF: null 367 | }; 368 | 369 | // Other move tables are computed on the fly 370 | moveTableParams = { 371 | // name: [scope, size] 372 | twist: ['corners', N_TWIST], 373 | flip: ['edges', N_FLIP], 374 | FRtoBR: ['edges', N_FRtoBR], 375 | URFtoDLF: ['corners', N_URFtoDLF], 376 | URtoDF: ['edges', N_URtoDF], 377 | URtoUL: ['edges', N_URtoUL], 378 | UBtoDF: ['edges', N_UBtoDF], 379 | mergeURtoDF: [] // handled specially 380 | }; 381 | 382 | Cube.computeMoveTables = function(...tables) { 383 | var len, m, name, scope, size, tableName; 384 | if (tables.length === 0) { 385 | tables = (function() { 386 | var results; 387 | results = []; 388 | for (name in moveTableParams) { 389 | results.push(name); 390 | } 391 | return results; 392 | })(); 393 | } 394 | for (m = 0, len = tables.length; m < len; m++) { 395 | tableName = tables[m]; 396 | if (this.moveTables[tableName] !== null) { 397 | // Already computed 398 | continue; 399 | } 400 | if (tableName === 'mergeURtoDF') { 401 | this.moveTables.mergeURtoDF = (function() { 402 | var UBtoDF, URtoUL, o, results; 403 | results = []; 404 | for (URtoUL = o = 0; o <= 335; URtoUL = ++o) { 405 | results.push((function() { 406 | var p, results1; 407 | results1 = []; 408 | for (UBtoDF = p = 0; p <= 335; UBtoDF = ++p) { 409 | results1.push(mergeURtoDF(URtoUL, UBtoDF)); 410 | } 411 | return results1; 412 | })()); 413 | } 414 | return results; 415 | })(); 416 | } else { 417 | [scope, size] = moveTableParams[tableName]; 418 | this.moveTables[tableName] = computeMoveTable(scope, tableName, size); 419 | } 420 | } 421 | return this; 422 | }; 423 | 424 | // Phase 1: All moves are valid 425 | allMoves1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; 426 | 427 | // The list of next valid phase 1 moves when the given face was turned 428 | // in the last move 429 | nextMoves1 = (function() { 430 | var face, lastFace, m, next, o, p, power, results; 431 | results = []; 432 | for (lastFace = m = 0; m <= 5; lastFace = ++m) { 433 | next = []; 434 | // Don't allow commuting moves, e.g. U U'. Also make sure that 435 | // opposite faces are always moved in the same order, i.e. allow 436 | // U D but no D U. This avoids sequences like U D U'. 437 | for (face = o = 0; o <= 5; face = ++o) { 438 | if (face !== lastFace && face !== lastFace - 3) { 439 | // single, double or inverse move 440 | for (power = p = 0; p <= 2; power = ++p) { 441 | next.push(face * 3 + power); 442 | } 443 | } 444 | } 445 | results.push(next); 446 | } 447 | return results; 448 | })(); 449 | 450 | // Phase 2: Double moves of all faces plus quarter moves of U and D 451 | allMoves2 = [0, 1, 2, 4, 7, 9, 10, 11, 13, 16]; 452 | 453 | nextMoves2 = (function() { 454 | var face, lastFace, len, m, next, o, p, power, powers, results; 455 | results = []; 456 | for (lastFace = m = 0; m <= 5; lastFace = ++m) { 457 | next = []; 458 | for (face = o = 0; o <= 5; face = ++o) { 459 | if (!(face !== lastFace && face !== lastFace - 3)) { 460 | continue; 461 | } 462 | // Allow all moves of U and D and double moves of others 463 | powers = face === 0 || face === 3 ? [0, 1, 2] : [1]; 464 | for (p = 0, len = powers.length; p < len; p++) { 465 | power = powers[p]; 466 | next.push(face * 3 + power); 467 | } 468 | } 469 | results.push(next); 470 | } 471 | return results; 472 | })(); 473 | 474 | // 8 values are encoded in one number 475 | pruning = function(table, index, value) { 476 | var pos, shift, slot; 477 | pos = index % 8; 478 | slot = index >> 3; 479 | shift = pos << 2; 480 | if (value != null) { 481 | // Set 482 | table[slot] &= ~(0xF << shift); 483 | table[slot] |= value << shift; 484 | return value; 485 | } else { 486 | // Get 487 | return (table[slot] & (0xF << shift)) >>> shift; 488 | } 489 | }; 490 | 491 | computePruningTable = function(phase, size, currentCoords, nextIndex) { 492 | var current, depth, done, index, len, m, move, moves, next, o, ref, table, x; 493 | // Initialize all values to 0xF 494 | table = (function() { 495 | var m, ref, results; 496 | results = []; 497 | for (x = m = 0, ref = Math.ceil(size / 8) - 1; (0 <= ref ? m <= ref : m >= ref); x = 0 <= ref ? ++m : --m) { 498 | results.push(0xFFFFFFFF); 499 | } 500 | return results; 501 | })(); 502 | if (phase === 1) { 503 | moves = allMoves1; 504 | } else { 505 | moves = allMoves2; 506 | } 507 | depth = 0; 508 | pruning(table, 0, depth); 509 | done = 1; 510 | // In each iteration, take each state found in the previous depth and 511 | // compute the next state. Stop when all states have been assigned a 512 | // depth. 513 | while (done !== size) { 514 | for (index = m = 0, ref = size - 1; (0 <= ref ? m <= ref : m >= ref); index = 0 <= ref ? ++m : --m) { 515 | if (!(pruning(table, index) === depth)) { 516 | continue; 517 | } 518 | current = currentCoords(index); 519 | for (o = 0, len = moves.length; o < len; o++) { 520 | move = moves[o]; 521 | next = nextIndex(current, move); 522 | if (pruning(table, next) === 0xF) { 523 | pruning(table, next, depth + 1); 524 | done++; 525 | } 526 | } 527 | } 528 | depth++; 529 | } 530 | return table; 531 | }; 532 | 533 | Cube.pruningTables = { 534 | sliceTwist: null, 535 | sliceFlip: null, 536 | sliceURFtoDLFParity: null, 537 | sliceURtoDFParity: null 538 | }; 539 | 540 | pruningTableParams = { 541 | // name: [phase, size, currentCoords, nextIndex] 542 | sliceTwist: [ 543 | 1, 544 | N_SLICE1 * N_TWIST, 545 | function(index) { 546 | return [index % N_SLICE1, 547 | index / N_SLICE1 | 0]; 548 | }, 549 | function(current, 550 | move) { 551 | var newSlice, 552 | newTwist, 553 | slice, 554 | twist; 555 | [slice, 556 | twist] = current; 557 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0; 558 | newTwist = Cube.moveTables.twist[twist][move]; 559 | return newTwist * N_SLICE1 + newSlice; 560 | } 561 | ], 562 | sliceFlip: [ 563 | 1, 564 | N_SLICE1 * N_FLIP, 565 | function(index) { 566 | return [index % N_SLICE1, 567 | index / N_SLICE1 | 0]; 568 | }, 569 | function(current, 570 | move) { 571 | var flip, 572 | newFlip, 573 | newSlice, 574 | slice; 575 | [slice, 576 | flip] = current; 577 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0; 578 | newFlip = Cube.moveTables.flip[flip][move]; 579 | return newFlip * N_SLICE1 + newSlice; 580 | } 581 | ], 582 | sliceURFtoDLFParity: [ 583 | 2, 584 | N_SLICE2 * N_URFtoDLF * N_PARITY, 585 | function(index) { 586 | return [index % 2, 587 | (index / 2 | 0) % N_SLICE2, 588 | (index / 2 | 0) / N_SLICE2 | 0]; 589 | }, 590 | function(current, 591 | move) { 592 | var URFtoDLF, 593 | newParity, 594 | newSlice, 595 | newURFtoDLF, 596 | parity, 597 | slice; 598 | [parity, 599 | slice, 600 | URFtoDLF] = current; 601 | newParity = Cube.moveTables.parity[parity][move]; 602 | newSlice = Cube.moveTables.FRtoBR[slice][move]; 603 | newURFtoDLF = Cube.moveTables.URFtoDLF[URFtoDLF][move]; 604 | return (newURFtoDLF * N_SLICE2 + newSlice) * 2 + newParity; 605 | } 606 | ], 607 | sliceURtoDFParity: [ 608 | 2, 609 | N_SLICE2 * N_URtoDF * N_PARITY, 610 | function(index) { 611 | return [index % 2, 612 | (index / 2 | 0) % N_SLICE2, 613 | (index / 2 | 0) / N_SLICE2 | 0]; 614 | }, 615 | function(current, 616 | move) { 617 | var URtoDF, 618 | newParity, 619 | newSlice, 620 | newURtoDF, 621 | parity, 622 | slice; 623 | [parity, 624 | slice, 625 | URtoDF] = current; 626 | newParity = Cube.moveTables.parity[parity][move]; 627 | newSlice = Cube.moveTables.FRtoBR[slice][move]; 628 | newURtoDF = Cube.moveTables.URtoDF[URtoDF][move]; 629 | return (newURtoDF * N_SLICE2 + newSlice) * 2 + newParity; 630 | } 631 | ] 632 | }; 633 | 634 | Cube.computePruningTables = function(...tables) { 635 | var len, m, name, params, tableName; 636 | if (tables.length === 0) { 637 | tables = (function() { 638 | var results; 639 | results = []; 640 | for (name in pruningTableParams) { 641 | results.push(name); 642 | } 643 | return results; 644 | })(); 645 | } 646 | for (m = 0, len = tables.length; m < len; m++) { 647 | tableName = tables[m]; 648 | if (this.pruningTables[tableName] !== null) { 649 | // Already computed 650 | continue; 651 | } 652 | params = pruningTableParams[tableName]; 653 | this.pruningTables[tableName] = computePruningTable(...params); 654 | } 655 | return this; 656 | }; 657 | 658 | Cube.initSolver = function() { 659 | Cube.computeMoveTables(); 660 | return Cube.computePruningTables(); 661 | }; 662 | 663 | Cube.prototype.solveUpright = function(maxDepth = 22) { 664 | var State, freeStates, moveNames, phase1, phase1search, phase2, phase2search, solution, state, x; 665 | // Names for all moves, i.e. U, U2, U', F, F2, ... 666 | moveNames = (function() { 667 | var face, faceName, m, o, power, powerName, result; 668 | faceName = ['U', 'R', 'F', 'D', 'L', 'B']; 669 | powerName = ['', '2', "'"]; 670 | result = []; 671 | for (face = m = 0; m <= 5; face = ++m) { 672 | for (power = o = 0; o <= 2; power = ++o) { 673 | result.push(faceName[face] + powerName[power]); 674 | } 675 | } 676 | return result; 677 | })(); 678 | State = class State { 679 | constructor(cube) { 680 | this.parent = null; 681 | this.lastMove = null; 682 | this.depth = 0; 683 | if (cube) { 684 | this.init(cube); 685 | } 686 | } 687 | 688 | init(cube) { 689 | // Phase 1 coordinates 690 | this.flip = cube.flip(); 691 | this.twist = cube.twist(); 692 | this.slice = cube.FRtoBR() / N_SLICE2 | 0; 693 | // Phase 2 coordinates 694 | this.parity = cube.cornerParity(); 695 | this.URFtoDLF = cube.URFtoDLF(); 696 | this.FRtoBR = cube.FRtoBR(); 697 | // These are later merged to URtoDF when phase 2 begins 698 | this.URtoUL = cube.URtoUL(); 699 | this.UBtoDF = cube.UBtoDF(); 700 | return this; 701 | } 702 | 703 | solution() { 704 | if (this.parent) { 705 | return this.parent.solution() + moveNames[this.lastMove] + ' '; 706 | } else { 707 | return ''; 708 | } 709 | } 710 | 711 | //# Helpers 712 | move(table, index, move) { 713 | return Cube.moveTables[table][index][move]; 714 | } 715 | 716 | pruning(table, index) { 717 | return pruning(Cube.pruningTables[table], index); 718 | } 719 | 720 | //# Phase 1 721 | 722 | // Return the next valid phase 1 moves for this state 723 | moves1() { 724 | if (this.lastMove !== null) { 725 | return nextMoves1[this.lastMove / 3 | 0]; 726 | } else { 727 | return allMoves1; 728 | } 729 | } 730 | 731 | // Compute the minimum number of moves to the end of phase 1 732 | minDist1() { 733 | var d1, d2; 734 | // The maximum number of moves to the end of phase 1 wrt. the 735 | // combination flip and slice coordinates only 736 | d1 = this.pruning('sliceFlip', N_SLICE1 * this.flip + this.slice); 737 | // The combination of twist and slice coordinates 738 | d2 = this.pruning('sliceTwist', N_SLICE1 * this.twist + this.slice); 739 | // The true minimal distance is the maximum of these two 740 | return max(d1, d2); 741 | } 742 | 743 | // Compute the next phase 1 state for the given move 744 | next1(move) { 745 | var next; 746 | next = freeStates.pop(); 747 | next.parent = this; 748 | next.lastMove = move; 749 | next.depth = this.depth + 1; 750 | next.flip = this.move('flip', this.flip, move); 751 | next.twist = this.move('twist', this.twist, move); 752 | next.slice = this.move('FRtoBR', this.slice * 24, move) / 24 | 0; 753 | return next; 754 | } 755 | 756 | //# Phase 2 757 | 758 | // Return the next valid phase 2 moves for this state 759 | moves2() { 760 | if (this.lastMove !== null) { 761 | return nextMoves2[this.lastMove / 3 | 0]; 762 | } else { 763 | return allMoves2; 764 | } 765 | } 766 | 767 | // Compute the minimum number of moves to the solved cube 768 | minDist2() { 769 | var d1, d2, index1, index2; 770 | index1 = (N_SLICE2 * this.URtoDF + this.FRtoBR) * N_PARITY + this.parity; 771 | d1 = this.pruning('sliceURtoDFParity', index1); 772 | index2 = (N_SLICE2 * this.URFtoDLF + this.FRtoBR) * N_PARITY + this.parity; 773 | d2 = this.pruning('sliceURFtoDLFParity', index2); 774 | return max(d1, d2); 775 | } 776 | 777 | // Initialize phase 2 coordinates 778 | init2(top = true) { 779 | if (this.parent === null) { 780 | return; 781 | } 782 | // For other states, the phase 2 state is computed based on 783 | // parent's state. 784 | // Already assigned for the initial state 785 | this.parent.init2(false); 786 | this.URFtoDLF = this.move('URFtoDLF', this.parent.URFtoDLF, this.lastMove); 787 | this.FRtoBR = this.move('FRtoBR', this.parent.FRtoBR, this.lastMove); 788 | this.parity = this.move('parity', this.parent.parity, this.lastMove); 789 | this.URtoUL = this.move('URtoUL', this.parent.URtoUL, this.lastMove); 790 | this.UBtoDF = this.move('UBtoDF', this.parent.UBtoDF, this.lastMove); 791 | if (top) { 792 | // This is the initial phase 2 state. Get the URtoDF coordinate 793 | // by merging URtoUL and UBtoDF 794 | return this.URtoDF = this.move('mergeURtoDF', this.URtoUL, this.UBtoDF); 795 | } 796 | } 797 | 798 | // Compute the next phase 2 state for the given move 799 | next2(move) { 800 | var next; 801 | next = freeStates.pop(); 802 | next.parent = this; 803 | next.lastMove = move; 804 | next.depth = this.depth + 1; 805 | next.URFtoDLF = this.move('URFtoDLF', this.URFtoDLF, move); 806 | next.FRtoBR = this.move('FRtoBR', this.FRtoBR, move); 807 | next.parity = this.move('parity', this.parity, move); 808 | next.URtoDF = this.move('URtoDF', this.URtoDF, move); 809 | return next; 810 | } 811 | 812 | }; 813 | solution = null; 814 | phase1search = function(state) { 815 | var depth, m, ref, results; 816 | results = []; 817 | for (depth = m = 1, ref = maxDepth; (1 <= ref ? m <= ref : m >= ref); depth = 1 <= ref ? ++m : --m) { 818 | phase1(state, depth); 819 | if (solution !== null) { 820 | break; 821 | } else { 822 | results.push(void 0); 823 | } 824 | } 825 | return results; 826 | }; 827 | phase1 = function(state, depth) { 828 | var len, m, move, next, ref, ref1, results; 829 | if (depth === 0) { 830 | if (state.minDist1() === 0) { 831 | // Make sure we don't start phase 2 with a phase 2 move as the 832 | // last move in phase 1, because phase 2 would then repeat the 833 | // same move. 834 | if (state.lastMove === null || (ref = state.lastMove, indexOf.call(allMoves2, ref) < 0)) { 835 | return phase2search(state); 836 | } 837 | } 838 | } else if (depth > 0) { 839 | if (state.minDist1() <= depth) { 840 | ref1 = state.moves1(); 841 | results = []; 842 | for (m = 0, len = ref1.length; m < len; m++) { 843 | move = ref1[m]; 844 | next = state.next1(move); 845 | phase1(next, depth - 1); 846 | freeStates.push(next); 847 | if (solution !== null) { 848 | break; 849 | } else { 850 | results.push(void 0); 851 | } 852 | } 853 | return results; 854 | } 855 | } 856 | }; 857 | phase2search = function(state) { 858 | var depth, m, ref, results; 859 | // Initialize phase 2 coordinates 860 | state.init2(); 861 | results = []; 862 | for (depth = m = 1, ref = maxDepth - state.depth; (1 <= ref ? m <= ref : m >= ref); depth = 1 <= ref ? ++m : --m) { 863 | phase2(state, depth); 864 | if (solution !== null) { 865 | break; 866 | } else { 867 | results.push(void 0); 868 | } 869 | } 870 | return results; 871 | }; 872 | phase2 = function(state, depth) { 873 | var len, m, move, next, ref, results; 874 | if (depth === 0) { 875 | if (state.minDist2() === 0) { 876 | return solution = state.solution(); 877 | } 878 | } else if (depth > 0) { 879 | if (state.minDist2() <= depth) { 880 | ref = state.moves2(); 881 | results = []; 882 | for (m = 0, len = ref.length; m < len; m++) { 883 | move = ref[m]; 884 | next = state.next2(move); 885 | phase2(next, depth - 1); 886 | freeStates.push(next); 887 | if (solution !== null) { 888 | break; 889 | } else { 890 | results.push(void 0); 891 | } 892 | } 893 | return results; 894 | } 895 | } 896 | }; 897 | freeStates = (function() { 898 | var m, ref, results; 899 | results = []; 900 | for (x = m = 0, ref = maxDepth + 1; (0 <= ref ? m <= ref : m >= ref); x = 0 <= ref ? ++m : --m) { 901 | results.push(new State()); 902 | } 903 | return results; 904 | })(); 905 | state = freeStates.pop().init(this); 906 | phase1search(state); 907 | freeStates.push(state); 908 | if (solution == null) { 909 | return null; 910 | } 911 | // Trim the trailing space and return 912 | return solution.trim(); 913 | }; 914 | 915 | faceNums = { 916 | U: 0, 917 | R: 1, 918 | F: 2, 919 | D: 3, 920 | L: 4, 921 | B: 5 922 | }; 923 | 924 | faceNames = { 925 | 0: 'U', 926 | 1: 'R', 927 | 2: 'F', 928 | 3: 'D', 929 | 4: 'L', 930 | 5: 'B' 931 | }; 932 | 933 | Cube.prototype.solve = function(maxDepth = 22) { 934 | var clone, len, m, move, ref, rotation, solution, upright, uprightSolution; 935 | clone = this.clone(); 936 | upright = clone.upright(); 937 | clone.move(upright); 938 | rotation = new Cube().move(upright).center; 939 | uprightSolution = clone.solveUpright(maxDepth); 940 | if (uprightSolution == null) { 941 | return null; 942 | } 943 | solution = []; 944 | ref = uprightSolution.split(' '); 945 | for (m = 0, len = ref.length; m < len; m++) { 946 | move = ref[m]; 947 | solution.push(faceNames[rotation[faceNums[move[0]]]]); 948 | if (move.length > 1) { 949 | solution[solution.length - 1] += move[1]; 950 | } 951 | } 952 | return solution.join(' '); 953 | }; 954 | 955 | Cube.scramble = function() { 956 | return Cube.inverse(Cube.random().solve()); 957 | }; 958 | 959 | }).call(this); 960 | -------------------------------------------------------------------------------- /docs/lib/worker.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var init, initialized, solve; 3 | 4 | importScripts('cube.js', 'solve.js'); 5 | 6 | initialized = false; 7 | 8 | init = function() { 9 | if (initialized) { 10 | return; 11 | } 12 | Cube.initSolver(); 13 | return initialized = true; 14 | }; 15 | 16 | solve = function(args) { 17 | var cube; 18 | if (!initialized) { 19 | return; 20 | } 21 | if (args.scramble) { 22 | cube = new Cube(); 23 | cube.move(args.scramble); 24 | } else if (args.cube) { 25 | cube = new Cube(args.cube); 26 | } 27 | return cube.solve(); 28 | }; 29 | 30 | self.onmessage = function(event) { 31 | var args; 32 | args = event.data; 33 | switch (args.cmd) { 34 | case 'init': 35 | init(); 36 | return self.postMessage({ 37 | cmd: 'init', 38 | status: 'ok' 39 | }); 40 | case 'solve': 41 | return self.postMessage({ 42 | cmd: 'solve', 43 | algorithm: solve(args) 44 | }); 45 | } 46 | }; 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const del = require('del'); 3 | const gulp = require('gulp'); 4 | const log = require('fancy-log'); 5 | const coffeelint = require('gulp-coffeelint'); 6 | const coffee = require('gulp-coffee'); 7 | const karma = require('karma'); 8 | 9 | const paths = { 10 | src: 'src', 11 | dist: 'lib', 12 | demo: 'docs' 13 | }; 14 | 15 | function clean() { 16 | return del([`./${paths.dist}`]); 17 | } 18 | 19 | function buildCoffee(dest) { 20 | return gulp.src(`./${paths.src}/*.coffee`) 21 | .pipe(coffeelint()) 22 | .pipe(coffeelint.reporter()) 23 | .pipe(coffee({ 24 | bare: false 25 | }).on('error', log)) 26 | .pipe(gulp.dest(dest)); 27 | } 28 | 29 | function build() { 30 | return buildCoffee(`./${paths.dist}/`); 31 | } 32 | 33 | function runTests(singleRun, done) { 34 | const localConfig = { 35 | configFile: path.join(__dirname, './karma.conf.coffee'), 36 | singleRun: singleRun, 37 | autoWatch: !singleRun 38 | }; 39 | 40 | const server = new karma.Server(localConfig, function(failCount) { 41 | done(failCount ? new Error(`Failed ${failCount} tests.`) : null); 42 | }); 43 | server.start(); 44 | } 45 | 46 | function cleanDemo() { 47 | return del([`./${paths.demo}/lib`]); 48 | } 49 | 50 | function demo() { 51 | return buildCoffee(`./${paths.demo}/lib/`); 52 | } 53 | 54 | gulp.task('test', function(done) { 55 | runTests(true, done); 56 | }); 57 | 58 | gulp.task('test:auto', function(done) { 59 | runTests(false, done); 60 | }); 61 | 62 | gulp.task('build', build); 63 | gulp.task('clean', clean); 64 | gulp.task('default', gulp.series(clean, build)); 65 | 66 | gulp.task('demo:clean', cleanDemo); 67 | gulp.task('demo:build', gulp.series(cleanDemo, demo)); 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/cube'); 2 | require('./lib/solve'); 3 | -------------------------------------------------------------------------------- /karma.conf.coffee: -------------------------------------------------------------------------------- 1 | # Karma configuration 2 | 3 | module.exports = (config) -> 4 | config.set 5 | 6 | # base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '' 8 | 9 | # frameworks to use 10 | # available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'] 12 | 13 | # list of files / patterns to load in the browser 14 | files: [ 15 | 'src/!(worker|async).coffee' 16 | 'spec/**/*.coffee' 17 | ] 18 | 19 | # list of files to exclude 20 | exclude: [] 21 | 22 | # preprocess matching files before serving them to the browser 23 | # available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 24 | preprocessors: 25 | '+(src|spec)/**/*.coffee': 'coffee' 26 | 27 | # test results reporter to use 28 | # possible values: 'dots', 'progress' 29 | # available reporters: https://npmjs.org/browse/keyword/karma-reporter 30 | reporters: ['dots'] 31 | 32 | # web server port 33 | port: 9876 34 | 35 | # enable / disable colors in the output (reporters and logs) 36 | colors: true 37 | 38 | # level of logging 39 | # possible values: 40 | # - config.LOG_DISABLE 41 | # - config.LOG_ERROR 42 | # - config.LOG_WARN 43 | # - config.LOG_INFO 44 | # - config.LOG_DEBUG 45 | logLevel: config.LOG_INFO 46 | 47 | # enable / disable watching file and executing tests whenever any file changes 48 | autoWatch: true 49 | 50 | # start these browsers 51 | # available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 52 | browsers: ['FirefoxHeadless'] 53 | 54 | # Continuous Integration mode 55 | # if true, Karma captures browsers, runs the tests and exits 56 | singleRun: false 57 | 58 | coffeePreprocessor: { 59 | options: { 60 | bare: false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/async.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Cube, Extend, key, value; 3 | 4 | Cube = this.Cube || require('./cube'); 5 | 6 | Extend = { 7 | asyncOK: !!window.Worker, 8 | _asyncSetup: function(workerURI) { 9 | if (this._worker) { 10 | return; 11 | } 12 | this._worker = new window.Worker(workerURI); 13 | this._worker.addEventListener('message', (e) => { 14 | return this._asyncEvent(e); 15 | }); 16 | return this._asyncCallbacks = {}; 17 | }, 18 | _asyncEvent: function(e) { 19 | var callback, callbacks; 20 | callbacks = this._asyncCallbacks[e.data.cmd]; 21 | if (!(callbacks && callbacks.length)) { 22 | return; 23 | } 24 | callback = callbacks[0]; 25 | callbacks.splice(0, 1); 26 | return callback(e.data); 27 | }, 28 | _asyncCallback: function(cmd, callback) { 29 | var base; 30 | (base = this._asyncCallbacks)[cmd] || (base[cmd] = []); 31 | return this._asyncCallbacks[cmd].push(callback); 32 | }, 33 | asyncInit: function(workerURI, callback) { 34 | this._asyncSetup(workerURI); 35 | this._asyncCallback('init', function() { 36 | return callback(); 37 | }); 38 | return this._worker.postMessage({ 39 | cmd: 'init' 40 | }); 41 | }, 42 | _asyncSolve: function(cube, callback) { 43 | this._asyncSetup(); 44 | this._asyncCallback('solve', function(data) { 45 | return callback(data.algorithm); 46 | }); 47 | return this._worker.postMessage({ 48 | cmd: 'solve', 49 | cube: cube.toJSON() 50 | }); 51 | }, 52 | asyncScramble: function(callback) { 53 | this._asyncSetup(); 54 | this._asyncCallback('solve', function(data) { 55 | return callback(Cube.inverse(data.algorithm)); 56 | }); 57 | return this._worker.postMessage({ 58 | cmd: 'solve', 59 | cube: Cube.random().toJSON() 60 | }); 61 | }, 62 | asyncSolve: function(callback) { 63 | return Cube._asyncSolve(this, callback); 64 | } 65 | }; 66 | 67 | for (key in Extend) { 68 | value = Extend[key]; 69 | Cube[key] = value; 70 | } 71 | 72 | }).call(this); 73 | -------------------------------------------------------------------------------- /lib/cube.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Centers 3 | var B, BL, BR, Cube, D, DB, DBL, DF, DFR, DL, DLF, DR, DRB, F, FL, FR, L, R, U, UB, UBR, UF, UFL, UL, ULB, UR, URF, centerColor, centerFacelet, cornerColor, cornerFacelet, edgeColor, edgeFacelet; 4 | 5 | [U, R, F, D, L, B] = [0, 1, 2, 3, 4, 5]; 6 | 7 | // Corners 8 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0, 1, 2, 3, 4, 5, 6, 7]; 9 | 10 | // Edges 11 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 12 | 13 | [centerFacelet, cornerFacelet, edgeFacelet] = (function() { 14 | var _B, _D, _F, _L, _R, _U; 15 | _U = function(x) { 16 | return x - 1; 17 | }; 18 | _R = function(x) { 19 | return _U(9) + x; 20 | }; 21 | _F = function(x) { 22 | return _R(9) + x; 23 | }; 24 | _D = function(x) { 25 | return _F(9) + x; 26 | }; 27 | _L = function(x) { 28 | return _D(9) + x; 29 | }; 30 | _B = function(x) { 31 | return _L(9) + x; 32 | }; 33 | return [ 34 | // Centers 35 | [4, 36 | 13, 37 | 22, 38 | 31, 39 | 40, 40 | 49], 41 | // Corners 42 | [[_U(9), 43 | _R(1), 44 | _F(3)], 45 | [_U(7), 46 | _F(1), 47 | _L(3)], 48 | [_U(1), 49 | _L(1), 50 | _B(3)], 51 | [_U(3), 52 | _B(1), 53 | _R(3)], 54 | [_D(3), 55 | _F(9), 56 | _R(7)], 57 | [_D(1), 58 | _L(9), 59 | _F(7)], 60 | [_D(7), 61 | _B(9), 62 | _L(7)], 63 | [_D(9), 64 | _R(9), 65 | _B(7)]], 66 | // Edges 67 | [[_U(6), 68 | _R(2)], 69 | [_U(8), 70 | _F(2)], 71 | [_U(4), 72 | _L(2)], 73 | [_U(2), 74 | _B(2)], 75 | [_D(6), 76 | _R(8)], 77 | [_D(2), 78 | _F(8)], 79 | [_D(4), 80 | _L(8)], 81 | [_D(8), 82 | _B(8)], 83 | [_F(6), 84 | _R(4)], 85 | [_F(4), 86 | _L(6)], 87 | [_B(6), 88 | _L(4)], 89 | [_B(4), 90 | _R(6)]] 91 | ]; 92 | })(); 93 | 94 | centerColor = ['U', 'R', 'F', 'D', 'L', 'B']; 95 | 96 | cornerColor = [['U', 'R', 'F'], ['U', 'F', 'L'], ['U', 'L', 'B'], ['U', 'B', 'R'], ['D', 'F', 'R'], ['D', 'L', 'F'], ['D', 'B', 'L'], ['D', 'R', 'B']]; 97 | 98 | edgeColor = [['U', 'R'], ['U', 'F'], ['U', 'L'], ['U', 'B'], ['D', 'R'], ['D', 'F'], ['D', 'L'], ['D', 'B'], ['F', 'R'], ['F', 'L'], ['B', 'L'], ['B', 'R']]; 99 | 100 | Cube = (function() { 101 | var faceNames, faceNums, parseAlg; 102 | 103 | class Cube { 104 | constructor(other) { 105 | var x; 106 | if (other != null) { 107 | this.init(other); 108 | } else { 109 | this.identity(); 110 | } 111 | // For moves to avoid allocating new objects each time 112 | this.newCenter = (function() { 113 | var k, results; 114 | results = []; 115 | for (x = k = 0; k <= 5; x = ++k) { 116 | results.push(0); 117 | } 118 | return results; 119 | })(); 120 | this.newCp = (function() { 121 | var k, results; 122 | results = []; 123 | for (x = k = 0; k <= 7; x = ++k) { 124 | results.push(0); 125 | } 126 | return results; 127 | })(); 128 | this.newEp = (function() { 129 | var k, results; 130 | results = []; 131 | for (x = k = 0; k <= 11; x = ++k) { 132 | results.push(0); 133 | } 134 | return results; 135 | })(); 136 | this.newCo = (function() { 137 | var k, results; 138 | results = []; 139 | for (x = k = 0; k <= 7; x = ++k) { 140 | results.push(0); 141 | } 142 | return results; 143 | })(); 144 | this.newEo = (function() { 145 | var k, results; 146 | results = []; 147 | for (x = k = 0; k <= 11; x = ++k) { 148 | results.push(0); 149 | } 150 | return results; 151 | })(); 152 | } 153 | 154 | init(state) { 155 | this.center = state.center.slice(0); 156 | this.co = state.co.slice(0); 157 | this.ep = state.ep.slice(0); 158 | this.cp = state.cp.slice(0); 159 | return this.eo = state.eo.slice(0); 160 | } 161 | 162 | identity() { 163 | var x; 164 | // Initialize to the identity cube 165 | this.center = [0, 1, 2, 3, 4, 5]; 166 | this.cp = [0, 1, 2, 3, 4, 5, 6, 7]; 167 | this.co = (function() { 168 | var k, results; 169 | results = []; 170 | for (x = k = 0; k <= 7; x = ++k) { 171 | results.push(0); 172 | } 173 | return results; 174 | })(); 175 | this.ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 176 | return this.eo = (function() { 177 | var k, results; 178 | results = []; 179 | for (x = k = 0; k <= 11; x = ++k) { 180 | results.push(0); 181 | } 182 | return results; 183 | })(); 184 | } 185 | 186 | toJSON() { 187 | return { 188 | center: this.center, 189 | cp: this.cp, 190 | co: this.co, 191 | ep: this.ep, 192 | eo: this.eo 193 | }; 194 | } 195 | 196 | asString() { 197 | var corner, edge, i, k, l, m, n, o, ori, p, result; 198 | result = []; 199 | for (i = k = 0; k <= 5; i = ++k) { 200 | result[9 * i + 4] = centerColor[this.center[i]]; 201 | } 202 | for (i = l = 0; l <= 7; i = ++l) { 203 | corner = this.cp[i]; 204 | ori = this.co[i]; 205 | for (n = m = 0; m <= 2; n = ++m) { 206 | result[cornerFacelet[i][(n + ori) % 3]] = cornerColor[corner][n]; 207 | } 208 | } 209 | for (i = o = 0; o <= 11; i = ++o) { 210 | edge = this.ep[i]; 211 | ori = this.eo[i]; 212 | for (n = p = 0; p <= 1; n = ++p) { 213 | result[edgeFacelet[i][(n + ori) % 2]] = edgeColor[edge][n]; 214 | } 215 | } 216 | return result.join(''); 217 | } 218 | 219 | static fromString(str) { 220 | var col1, col2, cube, i, j, k, l, m, o, ori, p, q, r, ref; 221 | cube = new Cube(); 222 | for (i = k = 0; k <= 5; i = ++k) { 223 | for (j = l = 0; l <= 5; j = ++l) { 224 | if (str[9 * i + 4] === centerColor[j]) { 225 | cube.center[i] = j; 226 | } 227 | } 228 | } 229 | for (i = m = 0; m <= 7; i = ++m) { 230 | for (ori = o = 0; o <= 2; ori = ++o) { 231 | if ((ref = str[cornerFacelet[i][ori]]) === 'U' || ref === 'D') { 232 | break; 233 | } 234 | } 235 | col1 = str[cornerFacelet[i][(ori + 1) % 3]]; 236 | col2 = str[cornerFacelet[i][(ori + 2) % 3]]; 237 | for (j = p = 0; p <= 7; j = ++p) { 238 | if (col1 === cornerColor[j][1] && col2 === cornerColor[j][2]) { 239 | cube.cp[i] = j; 240 | cube.co[i] = ori % 3; 241 | } 242 | } 243 | } 244 | for (i = q = 0; q <= 11; i = ++q) { 245 | for (j = r = 0; r <= 11; j = ++r) { 246 | if (str[edgeFacelet[i][0]] === edgeColor[j][0] && str[edgeFacelet[i][1]] === edgeColor[j][1]) { 247 | cube.ep[i] = j; 248 | cube.eo[i] = 0; 249 | break; 250 | } 251 | if (str[edgeFacelet[i][0]] === edgeColor[j][1] && str[edgeFacelet[i][1]] === edgeColor[j][0]) { 252 | cube.ep[i] = j; 253 | cube.eo[i] = 1; 254 | break; 255 | } 256 | } 257 | } 258 | return cube; 259 | } 260 | 261 | clone() { 262 | return new Cube(this.toJSON()); 263 | } 264 | 265 | // A class method returning a new random cube 266 | static random() { 267 | return new Cube().randomize(); 268 | } 269 | 270 | isSolved() { 271 | var c, cent, clone, e, k, l, m; 272 | clone = this.clone(); 273 | clone.move(clone.upright()); 274 | for (cent = k = 0; k <= 5; cent = ++k) { 275 | if (clone.center[cent] !== cent) { 276 | return false; 277 | } 278 | } 279 | for (c = l = 0; l <= 7; c = ++l) { 280 | if (clone.cp[c] !== c) { 281 | return false; 282 | } 283 | if (clone.co[c] !== 0) { 284 | return false; 285 | } 286 | } 287 | for (e = m = 0; m <= 11; e = ++m) { 288 | if (clone.ep[e] !== e) { 289 | return false; 290 | } 291 | if (clone.eo[e] !== 0) { 292 | return false; 293 | } 294 | } 295 | return true; 296 | } 297 | 298 | // Multiply this Cube with another Cube, restricted to centers. 299 | centerMultiply(other) { 300 | var from, k, to; 301 | for (to = k = 0; k <= 5; to = ++k) { 302 | from = other.center[to]; 303 | this.newCenter[to] = this.center[from]; 304 | } 305 | [this.center, this.newCenter] = [this.newCenter, this.center]; 306 | return this; 307 | } 308 | 309 | // Multiply this Cube with another Cube, restricted to corners. 310 | cornerMultiply(other) { 311 | var from, k, to; 312 | for (to = k = 0; k <= 7; to = ++k) { 313 | from = other.cp[to]; 314 | this.newCp[to] = this.cp[from]; 315 | this.newCo[to] = (this.co[from] + other.co[to]) % 3; 316 | } 317 | [this.cp, this.newCp] = [this.newCp, this.cp]; 318 | [this.co, this.newCo] = [this.newCo, this.co]; 319 | return this; 320 | } 321 | 322 | // Multiply this Cube with another Cube, restricted to edges 323 | edgeMultiply(other) { 324 | var from, k, to; 325 | for (to = k = 0; k <= 11; to = ++k) { 326 | from = other.ep[to]; 327 | this.newEp[to] = this.ep[from]; 328 | this.newEo[to] = (this.eo[from] + other.eo[to]) % 2; 329 | } 330 | [this.ep, this.newEp] = [this.newEp, this.ep]; 331 | [this.eo, this.newEo] = [this.newEo, this.eo]; 332 | return this; 333 | } 334 | 335 | // Multiply this cube with another Cube 336 | multiply(other) { 337 | this.centerMultiply(other); 338 | this.cornerMultiply(other); 339 | this.edgeMultiply(other); 340 | return this; 341 | } 342 | 343 | move(arg) { 344 | var face, k, l, len, move, power, ref, ref1, x; 345 | ref = parseAlg(arg); 346 | for (k = 0, len = ref.length; k < len; k++) { 347 | move = ref[k]; 348 | face = move / 3 | 0; 349 | power = move % 3; 350 | for (x = l = 0, ref1 = power; (0 <= ref1 ? l <= ref1 : l >= ref1); x = 0 <= ref1 ? ++l : --l) { 351 | this.multiply(Cube.moves[face]); 352 | } 353 | } 354 | return this; 355 | } 356 | 357 | upright() { 358 | var clone, i, j, k, l, result; 359 | clone = this.clone(); 360 | result = []; 361 | for (i = k = 0; k <= 5; i = ++k) { 362 | if (clone.center[i] === F) { 363 | break; 364 | } 365 | } 366 | switch (i) { 367 | case D: 368 | result.push("x"); 369 | break; 370 | case U: 371 | result.push("x'"); 372 | break; 373 | case B: 374 | result.push("x2"); 375 | break; 376 | case R: 377 | result.push("y"); 378 | break; 379 | case L: 380 | result.push("y'"); 381 | } 382 | if (result.length) { 383 | clone.move(result[0]); 384 | } 385 | for (j = l = 0; l <= 5; j = ++l) { 386 | if (clone.center[j] === U) { 387 | break; 388 | } 389 | } 390 | switch (j) { 391 | case L: 392 | result.push("z"); 393 | break; 394 | case R: 395 | result.push("z'"); 396 | break; 397 | case D: 398 | result.push("z2"); 399 | } 400 | return result.join(' '); 401 | } 402 | 403 | static inverse(arg) { 404 | var face, k, len, move, power, result, str; 405 | result = (function() { 406 | var k, len, ref, results; 407 | ref = parseAlg(arg); 408 | results = []; 409 | for (k = 0, len = ref.length; k < len; k++) { 410 | move = ref[k]; 411 | face = move / 3 | 0; 412 | power = move % 3; 413 | results.push(face * 3 + -(power - 1) + 1); 414 | } 415 | return results; 416 | })(); 417 | result.reverse(); 418 | if (typeof arg === 'string') { 419 | str = ''; 420 | for (k = 0, len = result.length; k < len; k++) { 421 | move = result[k]; 422 | face = move / 3 | 0; 423 | power = move % 3; 424 | str += faceNames[face]; 425 | if (power === 1) { 426 | str += '2'; 427 | } else if (power === 2) { 428 | str += "'"; 429 | } 430 | str += ' '; 431 | } 432 | return str.substring(0, str.length - 1); 433 | } else if (arg.length != null) { 434 | return result; 435 | } else { 436 | return result[0]; 437 | } 438 | } 439 | 440 | }; 441 | 442 | Cube.prototype.randomize = (function() { 443 | var arePermutationsValid, generateValidRandomOrientation, generateValidRandomPermutation, getNumSwaps, isOrientationValid, randint, randomizeOrientation, result, shuffle; 444 | randint = function(min, max) { 445 | return min + Math.floor(Math.random() * (max - min + 1)); 446 | }; 447 | // Fisher-Yates shuffle adapted from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array 448 | shuffle = function(array) { 449 | var currentIndex, randomIndex, temporaryValue; 450 | currentIndex = array.length; 451 | // While there remain elements to shuffle... 452 | while (currentIndex !== 0) { 453 | // Pick a remaining element... 454 | randomIndex = randint(0, currentIndex - 1); 455 | currentIndex -= 1; 456 | // And swap it with the current element. 457 | temporaryValue = array[currentIndex]; 458 | [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; 459 | } 460 | }; 461 | getNumSwaps = function(arr) { 462 | var cur, cycleLength, i, k, numSwaps, ref, seen, x; 463 | numSwaps = 0; 464 | seen = (function() { 465 | var k, ref, results; 466 | results = []; 467 | for (x = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); x = 0 <= ref ? ++k : --k) { 468 | results.push(false); 469 | } 470 | return results; 471 | })(); 472 | while (true) { 473 | // We compute the cycle decomposition 474 | cur = -1; 475 | for (i = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); i = 0 <= ref ? ++k : --k) { 476 | if (!seen[i]) { 477 | cur = i; 478 | break; 479 | } 480 | } 481 | if (cur === -1) { 482 | break; 483 | } 484 | cycleLength = 0; 485 | while (!seen[cur]) { 486 | seen[cur] = true; 487 | cycleLength++; 488 | cur = arr[cur]; 489 | } 490 | // A cycle is equivalent to cycleLength + 1 swaps 491 | numSwaps += cycleLength + 1; 492 | } 493 | return numSwaps; 494 | }; 495 | arePermutationsValid = function(cp, ep) { 496 | var numSwaps; 497 | numSwaps = getNumSwaps(ep) + getNumSwaps(cp); 498 | return numSwaps % 2 === 0; 499 | }; 500 | generateValidRandomPermutation = function(cp, ep) { 501 | // Each shuffle only takes around 12 operations and there's a 50% 502 | // chance of a valid permutation so it'll finish in very good time 503 | shuffle(ep); 504 | shuffle(cp); 505 | while (!arePermutationsValid(cp, ep)) { 506 | shuffle(ep); 507 | shuffle(cp); 508 | } 509 | }; 510 | randomizeOrientation = function(arr, numOrientations) { 511 | var i, k, ori, ref; 512 | ori = 0; 513 | for (i = k = 0, ref = arr.length - 1; (0 <= ref ? k <= ref : k >= ref); i = 0 <= ref ? ++k : --k) { 514 | ori += (arr[i] = randint(0, numOrientations - 1)); 515 | } 516 | }; 517 | isOrientationValid = function(arr, numOrientations) { 518 | return arr.reduce(function(a, b) { 519 | return a + b; 520 | }) % numOrientations === 0; 521 | }; 522 | generateValidRandomOrientation = function(co, eo) { 523 | // There is a 1/2 and 1/3 probably respectively of each of these 524 | // succeeding so the probability of them running 10 times before 525 | // success is already only 1% and only gets exponentially lower 526 | // and each generation is only in the 10s of operations which is nothing 527 | randomizeOrientation(co, 3); 528 | while (!isOrientationValid(co, 3)) { 529 | randomizeOrientation(co, 3); 530 | } 531 | randomizeOrientation(eo, 2); 532 | while (!isOrientationValid(eo, 2)) { 533 | randomizeOrientation(eo, 2); 534 | } 535 | }; 536 | result = function() { 537 | generateValidRandomPermutation(this.cp, this.ep); 538 | generateValidRandomOrientation(this.co, this.eo); 539 | return this; 540 | }; 541 | return result; 542 | })(); 543 | 544 | Cube.moves = [ 545 | { 546 | // U 547 | center: [0, 1, 2, 3, 4, 5], 548 | cp: [UBR, 549 | URF, 550 | UFL, 551 | ULB, 552 | DFR, 553 | DLF, 554 | DBL, 555 | DRB], 556 | co: [0, 557 | 0, 558 | 0, 559 | 0, 560 | 0, 561 | 0, 562 | 0, 563 | 0], 564 | ep: [UB, 565 | UR, 566 | UF, 567 | UL, 568 | DR, 569 | DF, 570 | DL, 571 | DB, 572 | FR, 573 | FL, 574 | BL, 575 | BR], 576 | eo: [0, 577 | 0, 578 | 0, 579 | 0, 580 | 0, 581 | 0, 582 | 0, 583 | 0, 584 | 0, 585 | 0, 586 | 0, 587 | 0] 588 | }, 589 | { 590 | // R 591 | center: [0, 1, 2, 3, 4, 5], 592 | cp: [DFR, 593 | UFL, 594 | ULB, 595 | URF, 596 | DRB, 597 | DLF, 598 | DBL, 599 | UBR], 600 | co: [2, 601 | 0, 602 | 0, 603 | 1, 604 | 1, 605 | 0, 606 | 0, 607 | 2], 608 | ep: [FR, 609 | UF, 610 | UL, 611 | UB, 612 | BR, 613 | DF, 614 | DL, 615 | DB, 616 | DR, 617 | FL, 618 | BL, 619 | UR], 620 | eo: [0, 621 | 0, 622 | 0, 623 | 0, 624 | 0, 625 | 0, 626 | 0, 627 | 0, 628 | 0, 629 | 0, 630 | 0, 631 | 0] 632 | }, 633 | { 634 | // F 635 | center: [0, 1, 2, 3, 4, 5], 636 | cp: [UFL, 637 | DLF, 638 | ULB, 639 | UBR, 640 | URF, 641 | DFR, 642 | DBL, 643 | DRB], 644 | co: [1, 645 | 2, 646 | 0, 647 | 0, 648 | 2, 649 | 1, 650 | 0, 651 | 0], 652 | ep: [UR, 653 | FL, 654 | UL, 655 | UB, 656 | DR, 657 | FR, 658 | DL, 659 | DB, 660 | UF, 661 | DF, 662 | BL, 663 | BR], 664 | eo: [0, 665 | 1, 666 | 0, 667 | 0, 668 | 0, 669 | 1, 670 | 0, 671 | 0, 672 | 1, 673 | 1, 674 | 0, 675 | 0] 676 | }, 677 | { 678 | // D 679 | center: [0, 1, 2, 3, 4, 5], 680 | cp: [URF, 681 | UFL, 682 | ULB, 683 | UBR, 684 | DLF, 685 | DBL, 686 | DRB, 687 | DFR], 688 | co: [0, 689 | 0, 690 | 0, 691 | 0, 692 | 0, 693 | 0, 694 | 0, 695 | 0], 696 | ep: [UR, 697 | UF, 698 | UL, 699 | UB, 700 | DF, 701 | DL, 702 | DB, 703 | DR, 704 | FR, 705 | FL, 706 | BL, 707 | BR], 708 | eo: [0, 709 | 0, 710 | 0, 711 | 0, 712 | 0, 713 | 0, 714 | 0, 715 | 0, 716 | 0, 717 | 0, 718 | 0, 719 | 0] 720 | }, 721 | { 722 | // L 723 | center: [0, 1, 2, 3, 4, 5], 724 | cp: [URF, 725 | ULB, 726 | DBL, 727 | UBR, 728 | DFR, 729 | UFL, 730 | DLF, 731 | DRB], 732 | co: [0, 733 | 1, 734 | 2, 735 | 0, 736 | 0, 737 | 2, 738 | 1, 739 | 0], 740 | ep: [UR, 741 | UF, 742 | BL, 743 | UB, 744 | DR, 745 | DF, 746 | FL, 747 | DB, 748 | FR, 749 | UL, 750 | DL, 751 | BR], 752 | eo: [0, 753 | 0, 754 | 0, 755 | 0, 756 | 0, 757 | 0, 758 | 0, 759 | 0, 760 | 0, 761 | 0, 762 | 0, 763 | 0] 764 | }, 765 | { 766 | // B 767 | center: [0, 1, 2, 3, 4, 5], 768 | cp: [URF, 769 | UFL, 770 | UBR, 771 | DRB, 772 | DFR, 773 | DLF, 774 | ULB, 775 | DBL], 776 | co: [0, 777 | 0, 778 | 1, 779 | 2, 780 | 0, 781 | 0, 782 | 2, 783 | 1], 784 | ep: [UR, 785 | UF, 786 | UL, 787 | BR, 788 | DR, 789 | DF, 790 | DL, 791 | BL, 792 | FR, 793 | FL, 794 | UB, 795 | DB], 796 | eo: [0, 797 | 0, 798 | 0, 799 | 1, 800 | 0, 801 | 0, 802 | 0, 803 | 1, 804 | 0, 805 | 0, 806 | 1, 807 | 1] 808 | }, 809 | { 810 | // E 811 | center: [U, 812 | F, 813 | L, 814 | D, 815 | B, 816 | R], 817 | cp: [URF, 818 | UFL, 819 | ULB, 820 | UBR, 821 | DFR, 822 | DLF, 823 | DBL, 824 | DRB], 825 | co: [0, 826 | 0, 827 | 0, 828 | 0, 829 | 0, 830 | 0, 831 | 0, 832 | 0], 833 | ep: [UR, 834 | UF, 835 | UL, 836 | UB, 837 | DR, 838 | DF, 839 | DL, 840 | DB, 841 | FL, 842 | BL, 843 | BR, 844 | FR], 845 | eo: [0, 846 | 0, 847 | 0, 848 | 0, 849 | 0, 850 | 0, 851 | 0, 852 | 0, 853 | 1, 854 | 1, 855 | 1, 856 | 1] 857 | }, 858 | { 859 | // M 860 | center: [B, 861 | R, 862 | U, 863 | F, 864 | L, 865 | D], 866 | cp: [URF, 867 | UFL, 868 | ULB, 869 | UBR, 870 | DFR, 871 | DLF, 872 | DBL, 873 | DRB], 874 | co: [0, 875 | 0, 876 | 0, 877 | 0, 878 | 0, 879 | 0, 880 | 0, 881 | 0], 882 | ep: [UR, 883 | UB, 884 | UL, 885 | DB, 886 | DR, 887 | UF, 888 | DL, 889 | DF, 890 | FR, 891 | FL, 892 | BL, 893 | BR], 894 | eo: [0, 895 | 1, 896 | 0, 897 | 1, 898 | 0, 899 | 1, 900 | 0, 901 | 1, 902 | 0, 903 | 0, 904 | 0, 905 | 0] 906 | }, 907 | { 908 | // S 909 | center: [L, 910 | U, 911 | F, 912 | R, 913 | D, 914 | B], 915 | cp: [URF, 916 | UFL, 917 | ULB, 918 | UBR, 919 | DFR, 920 | DLF, 921 | DBL, 922 | DRB], 923 | co: [0, 924 | 0, 925 | 0, 926 | 0, 927 | 0, 928 | 0, 929 | 0, 930 | 0], 931 | ep: [UL, 932 | UF, 933 | DL, 934 | UB, 935 | UR, 936 | DF, 937 | DR, 938 | DB, 939 | FR, 940 | FL, 941 | BL, 942 | BR], 943 | eo: [1, 944 | 0, 945 | 1, 946 | 0, 947 | 1, 948 | 0, 949 | 1, 950 | 0, 951 | 0, 952 | 0, 953 | 0, 954 | 0] 955 | } 956 | ]; 957 | 958 | faceNums = { 959 | U: 0, 960 | R: 1, 961 | F: 2, 962 | D: 3, 963 | L: 4, 964 | B: 5, 965 | E: 6, 966 | M: 7, 967 | S: 8, 968 | x: 9, 969 | y: 10, 970 | z: 11, 971 | u: 12, 972 | r: 13, 973 | f: 14, 974 | d: 15, 975 | l: 16, 976 | b: 17 977 | }; 978 | 979 | faceNames = { 980 | 0: 'U', 981 | 1: 'R', 982 | 2: 'F', 983 | 3: 'D', 984 | 4: 'L', 985 | 5: 'B', 986 | 6: 'E', 987 | 7: 'M', 988 | 8: 'S', 989 | 9: 'x', 990 | 10: 'y', 991 | 11: 'z', 992 | 12: 'u', 993 | 13: 'r', 994 | 14: 'f', 995 | 15: 'd', 996 | 16: 'l', 997 | 17: 'b' 998 | }; 999 | 1000 | parseAlg = function(arg) { 1001 | var k, len, move, part, power, ref, results; 1002 | if (typeof arg === 'string') { 1003 | ref = arg.split(/\s+/); 1004 | // String 1005 | results = []; 1006 | for (k = 0, len = ref.length; k < len; k++) { 1007 | part = ref[k]; 1008 | if (part.length === 0) { 1009 | // First and last can be empty 1010 | continue; 1011 | } 1012 | if (part.length > 2) { 1013 | throw new Error(`Invalid move: ${part}`); 1014 | } 1015 | move = faceNums[part[0]]; 1016 | if (move === void 0) { 1017 | throw new Error(`Invalid move: ${part}`); 1018 | } 1019 | if (part.length === 1) { 1020 | power = 0; 1021 | } else { 1022 | if (part[1] === '2') { 1023 | power = 1; 1024 | } else if (part[1] === "'") { 1025 | power = 2; 1026 | } else { 1027 | throw new Error(`Invalid move: ${part}`); 1028 | } 1029 | } 1030 | results.push(move * 3 + power); 1031 | } 1032 | return results; 1033 | } else if (arg.length != null) { 1034 | // Already an array 1035 | return arg; 1036 | } else { 1037 | // A single move 1038 | return [arg]; 1039 | } 1040 | }; 1041 | 1042 | // x 1043 | Cube.moves.push(new Cube().move("R M' L'").toJSON()); 1044 | 1045 | // y 1046 | Cube.moves.push(new Cube().move("U E' D'").toJSON()); 1047 | 1048 | // z 1049 | Cube.moves.push(new Cube().move("F S B'").toJSON()); 1050 | 1051 | // u 1052 | Cube.moves.push(new Cube().move("U E'").toJSON()); 1053 | 1054 | // r 1055 | Cube.moves.push(new Cube().move("R M'").toJSON()); 1056 | 1057 | // f 1058 | Cube.moves.push(new Cube().move("F S").toJSON()); 1059 | 1060 | // d 1061 | Cube.moves.push(new Cube().move("D E").toJSON()); 1062 | 1063 | // l 1064 | Cube.moves.push(new Cube().move("L M").toJSON()); 1065 | 1066 | // b 1067 | Cube.moves.push(new Cube().move("B S'").toJSON()); 1068 | 1069 | return Cube; 1070 | 1071 | }).call(this); 1072 | 1073 | //# Globals 1074 | if (typeof module !== "undefined" && module !== null) { 1075 | module.exports = Cube; 1076 | } else { 1077 | this.Cube = Cube; 1078 | } 1079 | 1080 | }).call(this); 1081 | -------------------------------------------------------------------------------- /lib/solve.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var B, BL, BR, Cnk, Cube, D, DB, DBL, DF, DFR, DL, DLF, DR, DRB, F, FL, FR, Include, L, N_FLIP, N_FRtoBR, N_PARITY, N_SLICE1, N_SLICE2, N_TWIST, N_UBtoDF, N_URFtoDLF, N_URtoDF, N_URtoUL, R, U, UB, UBR, UF, UFL, UL, ULB, UR, URF, allMoves1, allMoves2, computeMoveTable, computePruningTable, faceNames, faceNums, factorial, key, max, mergeURtoDF, moveTableParams, nextMoves1, nextMoves2, permutationIndex, pruning, pruningTableParams, rotateLeft, rotateRight, value, 3 | indexOf = [].indexOf; 4 | 5 | Cube = this.Cube || require('./cube'); 6 | 7 | // Centers 8 | [U, R, F, D, L, B] = [0, 1, 2, 3, 4, 5]; 9 | 10 | // Corners 11 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0, 1, 2, 3, 4, 5, 6, 7]; 12 | 13 | // Edges 14 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 15 | 16 | //# Helpers 17 | 18 | // n choose k, i.e. the binomial coeffiecient 19 | Cnk = function(n, k) { 20 | var i, j, s; 21 | if (n < k) { 22 | return 0; 23 | } 24 | if (k > n / 2) { 25 | k = n - k; 26 | } 27 | s = 1; 28 | i = n; 29 | j = 1; 30 | while (i !== n - k) { 31 | s *= i; 32 | s /= j; 33 | i--; 34 | j++; 35 | } 36 | return s; 37 | }; 38 | 39 | // n! 40 | factorial = function(n) { 41 | var f, i, m, ref; 42 | f = 1; 43 | for (i = m = 2, ref = n; (2 <= ref ? m <= ref : m >= ref); i = 2 <= ref ? ++m : --m) { 44 | f *= i; 45 | } 46 | return f; 47 | }; 48 | 49 | // Maximum of two values 50 | max = function(a, b) { 51 | if (a > b) { 52 | return a; 53 | } else { 54 | return b; 55 | } 56 | }; 57 | 58 | // Rotate elements between l and r left by one place 59 | rotateLeft = function(array, l, r) { 60 | var i, m, ref, ref1, tmp; 61 | tmp = array[l]; 62 | for (i = m = ref = l, ref1 = r - 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 63 | array[i] = array[i + 1]; 64 | } 65 | return array[r] = tmp; 66 | }; 67 | 68 | // Rotate elements between l and r right by one place 69 | rotateRight = function(array, l, r) { 70 | var i, m, ref, ref1, tmp; 71 | tmp = array[r]; 72 | for (i = m = ref = r, ref1 = l + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 73 | array[i] = array[i - 1]; 74 | } 75 | return array[l] = tmp; 76 | }; 77 | 78 | // Generate a function that computes permutation indices. 79 | 80 | // The permutation index actually encodes two indices: Combination, 81 | // i.e. positions of the cubies start..end (A) and their respective 82 | // permutation (B). The maximum value for B is 83 | 84 | // maxB = (end - start + 1)! 85 | 86 | // and the index is A * maxB + B 87 | permutationIndex = function(context, start, end, fromEnd = false) { 88 | var i, maxAll, maxB, maxOur, our, permName; 89 | maxOur = end - start; 90 | maxB = factorial(maxOur + 1); 91 | if (context === 'corners') { 92 | maxAll = 7; 93 | permName = 'cp'; 94 | } else { 95 | maxAll = 11; 96 | permName = 'ep'; 97 | } 98 | our = (function() { 99 | var m, ref, results; 100 | results = []; 101 | for (i = m = 0, ref = maxOur; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 102 | results.push(0); 103 | } 104 | return results; 105 | })(); 106 | return function(index) { 107 | var a, b, c, j, k, m, o, p, perm, q, ref, ref1, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, t, u, w, x, y, z; 108 | if (index != null) { 109 | for (i = m = 0, ref = maxOur; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 110 | // Reset our to [start..end] 111 | our[i] = i + start; 112 | } 113 | b = index % maxB; // permutation 114 | a = index / maxB | 0; // combination 115 | 116 | // Invalidate all edges 117 | perm = this[permName]; 118 | for (i = o = 0, ref1 = maxAll; (0 <= ref1 ? o <= ref1 : o >= ref1); i = 0 <= ref1 ? ++o : --o) { 119 | perm[i] = -1; 120 | } 121 | // Generate permutation from index b 122 | for (j = p = 1, ref2 = maxOur; (1 <= ref2 ? p <= ref2 : p >= ref2); j = 1 <= ref2 ? ++p : --p) { 123 | k = b % (j + 1); 124 | b = b / (j + 1) | 0; 125 | // TODO: Implement rotateRightBy(our, 0, j, k) 126 | while (k > 0) { 127 | rotateRight(our, 0, j); 128 | k--; 129 | } 130 | } 131 | // Generate combination and set our edges 132 | x = maxOur; 133 | if (fromEnd) { 134 | for (j = q = 0, ref3 = maxAll; (0 <= ref3 ? q <= ref3 : q >= ref3); j = 0 <= ref3 ? ++q : --q) { 135 | c = Cnk(maxAll - j, x + 1); 136 | if (a - c >= 0) { 137 | perm[j] = our[maxOur - x]; 138 | a -= c; 139 | x--; 140 | } 141 | } 142 | } else { 143 | for (j = t = ref4 = maxAll; (ref4 <= 0 ? t <= 0 : t >= 0); j = ref4 <= 0 ? ++t : --t) { 144 | c = Cnk(j, x + 1); 145 | if (a - c >= 0) { 146 | perm[j] = our[x]; 147 | a -= c; 148 | x--; 149 | } 150 | } 151 | } 152 | return this; 153 | } else { 154 | perm = this[permName]; 155 | for (i = u = 0, ref5 = maxOur; (0 <= ref5 ? u <= ref5 : u >= ref5); i = 0 <= ref5 ? ++u : --u) { 156 | our[i] = -1; 157 | } 158 | a = b = x = 0; 159 | // Compute the index a < ((maxAll + 1) choose (maxOur + 1)) and 160 | // the permutation 161 | if (fromEnd) { 162 | for (j = w = ref6 = maxAll; (ref6 <= 0 ? w <= 0 : w >= 0); j = ref6 <= 0 ? ++w : --w) { 163 | if ((start <= (ref7 = perm[j]) && ref7 <= end)) { 164 | a += Cnk(maxAll - j, x + 1); 165 | our[maxOur - x] = perm[j]; 166 | x++; 167 | } 168 | } 169 | } else { 170 | for (j = y = 0, ref8 = maxAll; (0 <= ref8 ? y <= ref8 : y >= ref8); j = 0 <= ref8 ? ++y : --y) { 171 | if ((start <= (ref9 = perm[j]) && ref9 <= end)) { 172 | a += Cnk(j, x + 1); 173 | our[x] = perm[j]; 174 | x++; 175 | } 176 | } 177 | } 178 | // Compute the index b < (maxOur + 1)! for the permutation 179 | for (j = z = ref10 = maxOur; (ref10 <= 0 ? z <= 0 : z >= 0); j = ref10 <= 0 ? ++z : --z) { 180 | k = 0; 181 | while (our[j] !== start + j) { 182 | rotateLeft(our, 0, j); 183 | k++; 184 | } 185 | b = (j + 1) * b + k; 186 | } 187 | return a * maxB + b; 188 | } 189 | }; 190 | }; 191 | 192 | Include = { 193 | // The twist of the 8 corners, 0 <= twist < 3^7. The orientation of 194 | // the DRB corner is fully determined by the orientation of the other 195 | // corners. 196 | twist: function(twist) { 197 | var i, m, o, ori, parity, v; 198 | if (twist != null) { 199 | parity = 0; 200 | for (i = m = 6; m >= 0; i = --m) { 201 | ori = twist % 3; 202 | twist = (twist / 3) | 0; 203 | this.co[i] = ori; 204 | parity += ori; 205 | } 206 | this.co[7] = (3 - parity % 3) % 3; 207 | return this; 208 | } else { 209 | v = 0; 210 | for (i = o = 0; o <= 6; i = ++o) { 211 | v = 3 * v + this.co[i]; 212 | } 213 | return v; 214 | } 215 | }, 216 | // The flip of the 12 edges, 0 <= flip < 2^11. The orientation of the 217 | // BR edge is fully determined by the orientation of the other edges. 218 | flip: function(flip) { 219 | var i, m, o, ori, parity, v; 220 | if (flip != null) { 221 | parity = 0; 222 | for (i = m = 10; m >= 0; i = --m) { 223 | ori = flip % 2; 224 | flip = flip / 2 | 0; 225 | this.eo[i] = ori; 226 | parity += ori; 227 | } 228 | this.eo[11] = (2 - parity % 2) % 2; 229 | return this; 230 | } else { 231 | v = 0; 232 | for (i = o = 0; o <= 10; i = ++o) { 233 | v = 2 * v + this.eo[i]; 234 | } 235 | return v; 236 | } 237 | }, 238 | // Parity of the corner permutation 239 | cornerParity: function() { 240 | var i, j, m, o, ref, ref1, ref2, ref3, s; 241 | s = 0; 242 | for (i = m = ref = DRB, ref1 = URF + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 243 | for (j = o = ref2 = i - 1, ref3 = URF; (ref2 <= ref3 ? o <= ref3 : o >= ref3); j = ref2 <= ref3 ? ++o : --o) { 244 | if (this.cp[j] > this.cp[i]) { 245 | s++; 246 | } 247 | } 248 | } 249 | return s % 2; 250 | }, 251 | // Parity of the edges permutation. Parity of corners and edges are 252 | // the same if the cube is solvable. 253 | edgeParity: function() { 254 | var i, j, m, o, ref, ref1, ref2, ref3, s; 255 | s = 0; 256 | for (i = m = ref = BR, ref1 = UR + 1; (ref <= ref1 ? m <= ref1 : m >= ref1); i = ref <= ref1 ? ++m : --m) { 257 | for (j = o = ref2 = i - 1, ref3 = UR; (ref2 <= ref3 ? o <= ref3 : o >= ref3); j = ref2 <= ref3 ? ++o : --o) { 258 | if (this.ep[j] > this.ep[i]) { 259 | s++; 260 | } 261 | } 262 | } 263 | return s % 2; 264 | }, 265 | // Permutation of the six corners URF, UFL, ULB, UBR, DFR, DLF 266 | URFtoDLF: permutationIndex('corners', URF, DLF), 267 | // Permutation of the three edges UR, UF, UL 268 | URtoUL: permutationIndex('edges', UR, UL), 269 | // Permutation of the three edges UB, DR, DF 270 | UBtoDF: permutationIndex('edges', UB, DF), 271 | // Permutation of the six edges UR, UF, UL, UB, DR, DF 272 | URtoDF: permutationIndex('edges', UR, DF), 273 | // Permutation of the equator slice edges FR, FL, BL and BR 274 | FRtoBR: permutationIndex('edges', FR, BR, true) 275 | }; 276 | 277 | for (key in Include) { 278 | value = Include[key]; 279 | Cube.prototype[key] = value; 280 | } 281 | 282 | computeMoveTable = function(context, coord, size) { 283 | var apply, cube, i, inner, j, k, m, move, o, p, ref, results; 284 | // Loop through all valid values for the coordinate, setting cube's 285 | // state in each iteration. Then apply each of the 18 moves to the 286 | // cube, and compute the resulting coordinate. 287 | apply = context === 'corners' ? 'cornerMultiply' : 'edgeMultiply'; 288 | cube = new Cube(); 289 | results = []; 290 | for (i = m = 0, ref = size - 1; (0 <= ref ? m <= ref : m >= ref); i = 0 <= ref ? ++m : --m) { 291 | cube[coord](i); 292 | inner = []; 293 | for (j = o = 0; o <= 5; j = ++o) { 294 | move = Cube.moves[j]; 295 | for (k = p = 0; p <= 2; k = ++p) { 296 | cube[apply](move); 297 | inner.push(cube[coord]()); 298 | } 299 | // 4th face turn restores the cube 300 | cube[apply](move); 301 | } 302 | results.push(inner); 303 | } 304 | return results; 305 | }; 306 | 307 | // Because we only have the phase 2 URtoDF coordinates, we need to 308 | // merge the URtoUL and UBtoDF coordinates to URtoDF in the beginning 309 | // of phase 2. 310 | mergeURtoDF = (function() { 311 | var a, b; 312 | a = new Cube(); 313 | b = new Cube(); 314 | return function(URtoUL, UBtoDF) { 315 | var i, m; 316 | // Collisions can be found because unset are set to -1 317 | a.URtoUL(URtoUL); 318 | b.UBtoDF(UBtoDF); 319 | for (i = m = 0; m <= 7; i = ++m) { 320 | if (a.ep[i] !== -1) { 321 | if (b.ep[i] !== -1) { 322 | return -1; // collision 323 | } else { 324 | b.ep[i] = a.ep[i]; 325 | } 326 | } 327 | } 328 | return b.URtoDF(); 329 | }; 330 | })(); 331 | 332 | N_TWIST = 2187; // 3^7 corner orientations 333 | 334 | N_FLIP = 2048; // 2^11 possible edge flips 335 | 336 | N_PARITY = 2; // 2 possible parities 337 | 338 | N_FRtoBR = 11880; // 12!/(12-4)! permutations of FR..BR edges 339 | 340 | N_SLICE1 = 495; // (12 choose 4) possible positions of FR..BR edges 341 | 342 | N_SLICE2 = 24; // 4! permutations of FR..BR edges in phase 2 343 | 344 | N_URFtoDLF = 20160; // 8!/(8-6)! permutations of URF..DLF corners 345 | 346 | 347 | // The URtoDF move table is only computed for phase 2 because the full 348 | // table would have >650000 entries 349 | N_URtoDF = 20160; // 8!/(8-6)! permutation of UR..DF edges in phase 2 350 | 351 | N_URtoUL = 1320; // 12!/(12-3)! permutations of UR..UL edges 352 | 353 | N_UBtoDF = 1320; // 12!/(12-3)! permutations of UB..DF edges 354 | 355 | 356 | // The move table for parity is so small that it's included here 357 | Cube.moveTables = { 358 | parity: [[1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]], 359 | twist: null, 360 | flip: null, 361 | FRtoBR: null, 362 | URFtoDLF: null, 363 | URtoDF: null, 364 | URtoUL: null, 365 | UBtoDF: null, 366 | mergeURtoDF: null 367 | }; 368 | 369 | // Other move tables are computed on the fly 370 | moveTableParams = { 371 | // name: [scope, size] 372 | twist: ['corners', N_TWIST], 373 | flip: ['edges', N_FLIP], 374 | FRtoBR: ['edges', N_FRtoBR], 375 | URFtoDLF: ['corners', N_URFtoDLF], 376 | URtoDF: ['edges', N_URtoDF], 377 | URtoUL: ['edges', N_URtoUL], 378 | UBtoDF: ['edges', N_UBtoDF], 379 | mergeURtoDF: [] // handled specially 380 | }; 381 | 382 | Cube.computeMoveTables = function(...tables) { 383 | var len, m, name, scope, size, tableName; 384 | if (tables.length === 0) { 385 | tables = (function() { 386 | var results; 387 | results = []; 388 | for (name in moveTableParams) { 389 | results.push(name); 390 | } 391 | return results; 392 | })(); 393 | } 394 | for (m = 0, len = tables.length; m < len; m++) { 395 | tableName = tables[m]; 396 | if (this.moveTables[tableName] !== null) { 397 | // Already computed 398 | continue; 399 | } 400 | if (tableName === 'mergeURtoDF') { 401 | this.moveTables.mergeURtoDF = (function() { 402 | var UBtoDF, URtoUL, o, results; 403 | results = []; 404 | for (URtoUL = o = 0; o <= 335; URtoUL = ++o) { 405 | results.push((function() { 406 | var p, results1; 407 | results1 = []; 408 | for (UBtoDF = p = 0; p <= 335; UBtoDF = ++p) { 409 | results1.push(mergeURtoDF(URtoUL, UBtoDF)); 410 | } 411 | return results1; 412 | })()); 413 | } 414 | return results; 415 | })(); 416 | } else { 417 | [scope, size] = moveTableParams[tableName]; 418 | this.moveTables[tableName] = computeMoveTable(scope, tableName, size); 419 | } 420 | } 421 | return this; 422 | }; 423 | 424 | // Phase 1: All moves are valid 425 | allMoves1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; 426 | 427 | // The list of next valid phase 1 moves when the given face was turned 428 | // in the last move 429 | nextMoves1 = (function() { 430 | var face, lastFace, m, next, o, p, power, results; 431 | results = []; 432 | for (lastFace = m = 0; m <= 5; lastFace = ++m) { 433 | next = []; 434 | // Don't allow commuting moves, e.g. U U'. Also make sure that 435 | // opposite faces are always moved in the same order, i.e. allow 436 | // U D but no D U. This avoids sequences like U D U'. 437 | for (face = o = 0; o <= 5; face = ++o) { 438 | if (face !== lastFace && face !== lastFace - 3) { 439 | // single, double or inverse move 440 | for (power = p = 0; p <= 2; power = ++p) { 441 | next.push(face * 3 + power); 442 | } 443 | } 444 | } 445 | results.push(next); 446 | } 447 | return results; 448 | })(); 449 | 450 | // Phase 2: Double moves of all faces plus quarter moves of U and D 451 | allMoves2 = [0, 1, 2, 4, 7, 9, 10, 11, 13, 16]; 452 | 453 | nextMoves2 = (function() { 454 | var face, lastFace, len, m, next, o, p, power, powers, results; 455 | results = []; 456 | for (lastFace = m = 0; m <= 5; lastFace = ++m) { 457 | next = []; 458 | for (face = o = 0; o <= 5; face = ++o) { 459 | if (!(face !== lastFace && face !== lastFace - 3)) { 460 | continue; 461 | } 462 | // Allow all moves of U and D and double moves of others 463 | powers = face === 0 || face === 3 ? [0, 1, 2] : [1]; 464 | for (p = 0, len = powers.length; p < len; p++) { 465 | power = powers[p]; 466 | next.push(face * 3 + power); 467 | } 468 | } 469 | results.push(next); 470 | } 471 | return results; 472 | })(); 473 | 474 | // 8 values are encoded in one number 475 | pruning = function(table, index, value) { 476 | var pos, shift, slot; 477 | pos = index % 8; 478 | slot = index >> 3; 479 | shift = pos << 2; 480 | if (value != null) { 481 | // Set 482 | table[slot] &= ~(0xF << shift); 483 | table[slot] |= value << shift; 484 | return value; 485 | } else { 486 | // Get 487 | return (table[slot] & (0xF << shift)) >>> shift; 488 | } 489 | }; 490 | 491 | computePruningTable = function(phase, size, currentCoords, nextIndex) { 492 | var current, depth, done, index, len, m, move, moves, next, o, ref, table, x; 493 | // Initialize all values to 0xF 494 | table = (function() { 495 | var m, ref, results; 496 | results = []; 497 | for (x = m = 0, ref = Math.ceil(size / 8) - 1; (0 <= ref ? m <= ref : m >= ref); x = 0 <= ref ? ++m : --m) { 498 | results.push(0xFFFFFFFF); 499 | } 500 | return results; 501 | })(); 502 | if (phase === 1) { 503 | moves = allMoves1; 504 | } else { 505 | moves = allMoves2; 506 | } 507 | depth = 0; 508 | pruning(table, 0, depth); 509 | done = 1; 510 | // In each iteration, take each state found in the previous depth and 511 | // compute the next state. Stop when all states have been assigned a 512 | // depth. 513 | while (done !== size) { 514 | for (index = m = 0, ref = size - 1; (0 <= ref ? m <= ref : m >= ref); index = 0 <= ref ? ++m : --m) { 515 | if (!(pruning(table, index) === depth)) { 516 | continue; 517 | } 518 | current = currentCoords(index); 519 | for (o = 0, len = moves.length; o < len; o++) { 520 | move = moves[o]; 521 | next = nextIndex(current, move); 522 | if (pruning(table, next) === 0xF) { 523 | pruning(table, next, depth + 1); 524 | done++; 525 | } 526 | } 527 | } 528 | depth++; 529 | } 530 | return table; 531 | }; 532 | 533 | Cube.pruningTables = { 534 | sliceTwist: null, 535 | sliceFlip: null, 536 | sliceURFtoDLFParity: null, 537 | sliceURtoDFParity: null 538 | }; 539 | 540 | pruningTableParams = { 541 | // name: [phase, size, currentCoords, nextIndex] 542 | sliceTwist: [ 543 | 1, 544 | N_SLICE1 * N_TWIST, 545 | function(index) { 546 | return [index % N_SLICE1, 547 | index / N_SLICE1 | 0]; 548 | }, 549 | function(current, 550 | move) { 551 | var newSlice, 552 | newTwist, 553 | slice, 554 | twist; 555 | [slice, 556 | twist] = current; 557 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0; 558 | newTwist = Cube.moveTables.twist[twist][move]; 559 | return newTwist * N_SLICE1 + newSlice; 560 | } 561 | ], 562 | sliceFlip: [ 563 | 1, 564 | N_SLICE1 * N_FLIP, 565 | function(index) { 566 | return [index % N_SLICE1, 567 | index / N_SLICE1 | 0]; 568 | }, 569 | function(current, 570 | move) { 571 | var flip, 572 | newFlip, 573 | newSlice, 574 | slice; 575 | [slice, 576 | flip] = current; 577 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0; 578 | newFlip = Cube.moveTables.flip[flip][move]; 579 | return newFlip * N_SLICE1 + newSlice; 580 | } 581 | ], 582 | sliceURFtoDLFParity: [ 583 | 2, 584 | N_SLICE2 * N_URFtoDLF * N_PARITY, 585 | function(index) { 586 | return [index % 2, 587 | (index / 2 | 0) % N_SLICE2, 588 | (index / 2 | 0) / N_SLICE2 | 0]; 589 | }, 590 | function(current, 591 | move) { 592 | var URFtoDLF, 593 | newParity, 594 | newSlice, 595 | newURFtoDLF, 596 | parity, 597 | slice; 598 | [parity, 599 | slice, 600 | URFtoDLF] = current; 601 | newParity = Cube.moveTables.parity[parity][move]; 602 | newSlice = Cube.moveTables.FRtoBR[slice][move]; 603 | newURFtoDLF = Cube.moveTables.URFtoDLF[URFtoDLF][move]; 604 | return (newURFtoDLF * N_SLICE2 + newSlice) * 2 + newParity; 605 | } 606 | ], 607 | sliceURtoDFParity: [ 608 | 2, 609 | N_SLICE2 * N_URtoDF * N_PARITY, 610 | function(index) { 611 | return [index % 2, 612 | (index / 2 | 0) % N_SLICE2, 613 | (index / 2 | 0) / N_SLICE2 | 0]; 614 | }, 615 | function(current, 616 | move) { 617 | var URtoDF, 618 | newParity, 619 | newSlice, 620 | newURtoDF, 621 | parity, 622 | slice; 623 | [parity, 624 | slice, 625 | URtoDF] = current; 626 | newParity = Cube.moveTables.parity[parity][move]; 627 | newSlice = Cube.moveTables.FRtoBR[slice][move]; 628 | newURtoDF = Cube.moveTables.URtoDF[URtoDF][move]; 629 | return (newURtoDF * N_SLICE2 + newSlice) * 2 + newParity; 630 | } 631 | ] 632 | }; 633 | 634 | Cube.computePruningTables = function(...tables) { 635 | var len, m, name, params, tableName; 636 | if (tables.length === 0) { 637 | tables = (function() { 638 | var results; 639 | results = []; 640 | for (name in pruningTableParams) { 641 | results.push(name); 642 | } 643 | return results; 644 | })(); 645 | } 646 | for (m = 0, len = tables.length; m < len; m++) { 647 | tableName = tables[m]; 648 | if (this.pruningTables[tableName] !== null) { 649 | // Already computed 650 | continue; 651 | } 652 | params = pruningTableParams[tableName]; 653 | this.pruningTables[tableName] = computePruningTable(...params); 654 | } 655 | return this; 656 | }; 657 | 658 | Cube.initSolver = function() { 659 | Cube.computeMoveTables(); 660 | return Cube.computePruningTables(); 661 | }; 662 | 663 | Cube.prototype.solveUpright = function(maxDepth = 22) { 664 | var State, freeStates, moveNames, phase1, phase1search, phase2, phase2search, solution, state, x; 665 | // Names for all moves, i.e. U, U2, U', F, F2, ... 666 | moveNames = (function() { 667 | var face, faceName, m, o, power, powerName, result; 668 | faceName = ['U', 'R', 'F', 'D', 'L', 'B']; 669 | powerName = ['', '2', "'"]; 670 | result = []; 671 | for (face = m = 0; m <= 5; face = ++m) { 672 | for (power = o = 0; o <= 2; power = ++o) { 673 | result.push(faceName[face] + powerName[power]); 674 | } 675 | } 676 | return result; 677 | })(); 678 | State = class State { 679 | constructor(cube) { 680 | this.parent = null; 681 | this.lastMove = null; 682 | this.depth = 0; 683 | if (cube) { 684 | this.init(cube); 685 | } 686 | } 687 | 688 | init(cube) { 689 | // Phase 1 coordinates 690 | this.flip = cube.flip(); 691 | this.twist = cube.twist(); 692 | this.slice = cube.FRtoBR() / N_SLICE2 | 0; 693 | // Phase 2 coordinates 694 | this.parity = cube.cornerParity(); 695 | this.URFtoDLF = cube.URFtoDLF(); 696 | this.FRtoBR = cube.FRtoBR(); 697 | // These are later merged to URtoDF when phase 2 begins 698 | this.URtoUL = cube.URtoUL(); 699 | this.UBtoDF = cube.UBtoDF(); 700 | return this; 701 | } 702 | 703 | solution() { 704 | if (this.parent) { 705 | return this.parent.solution() + moveNames[this.lastMove] + ' '; 706 | } else { 707 | return ''; 708 | } 709 | } 710 | 711 | //# Helpers 712 | move(table, index, move) { 713 | return Cube.moveTables[table][index][move]; 714 | } 715 | 716 | pruning(table, index) { 717 | return pruning(Cube.pruningTables[table], index); 718 | } 719 | 720 | //# Phase 1 721 | 722 | // Return the next valid phase 1 moves for this state 723 | moves1() { 724 | if (this.lastMove !== null) { 725 | return nextMoves1[this.lastMove / 3 | 0]; 726 | } else { 727 | return allMoves1; 728 | } 729 | } 730 | 731 | // Compute the minimum number of moves to the end of phase 1 732 | minDist1() { 733 | var d1, d2; 734 | // The maximum number of moves to the end of phase 1 wrt. the 735 | // combination flip and slice coordinates only 736 | d1 = this.pruning('sliceFlip', N_SLICE1 * this.flip + this.slice); 737 | // The combination of twist and slice coordinates 738 | d2 = this.pruning('sliceTwist', N_SLICE1 * this.twist + this.slice); 739 | // The true minimal distance is the maximum of these two 740 | return max(d1, d2); 741 | } 742 | 743 | // Compute the next phase 1 state for the given move 744 | next1(move) { 745 | var next; 746 | next = freeStates.pop(); 747 | next.parent = this; 748 | next.lastMove = move; 749 | next.depth = this.depth + 1; 750 | next.flip = this.move('flip', this.flip, move); 751 | next.twist = this.move('twist', this.twist, move); 752 | next.slice = this.move('FRtoBR', this.slice * 24, move) / 24 | 0; 753 | return next; 754 | } 755 | 756 | //# Phase 2 757 | 758 | // Return the next valid phase 2 moves for this state 759 | moves2() { 760 | if (this.lastMove !== null) { 761 | return nextMoves2[this.lastMove / 3 | 0]; 762 | } else { 763 | return allMoves2; 764 | } 765 | } 766 | 767 | // Compute the minimum number of moves to the solved cube 768 | minDist2() { 769 | var d1, d2, index1, index2; 770 | index1 = (N_SLICE2 * this.URtoDF + this.FRtoBR) * N_PARITY + this.parity; 771 | d1 = this.pruning('sliceURtoDFParity', index1); 772 | index2 = (N_SLICE2 * this.URFtoDLF + this.FRtoBR) * N_PARITY + this.parity; 773 | d2 = this.pruning('sliceURFtoDLFParity', index2); 774 | return max(d1, d2); 775 | } 776 | 777 | // Initialize phase 2 coordinates 778 | init2(top = true) { 779 | if (this.parent === null) { 780 | return; 781 | } 782 | // For other states, the phase 2 state is computed based on 783 | // parent's state. 784 | // Already assigned for the initial state 785 | this.parent.init2(false); 786 | this.URFtoDLF = this.move('URFtoDLF', this.parent.URFtoDLF, this.lastMove); 787 | this.FRtoBR = this.move('FRtoBR', this.parent.FRtoBR, this.lastMove); 788 | this.parity = this.move('parity', this.parent.parity, this.lastMove); 789 | this.URtoUL = this.move('URtoUL', this.parent.URtoUL, this.lastMove); 790 | this.UBtoDF = this.move('UBtoDF', this.parent.UBtoDF, this.lastMove); 791 | if (top) { 792 | // This is the initial phase 2 state. Get the URtoDF coordinate 793 | // by merging URtoUL and UBtoDF 794 | return this.URtoDF = this.move('mergeURtoDF', this.URtoUL, this.UBtoDF); 795 | } 796 | } 797 | 798 | // Compute the next phase 2 state for the given move 799 | next2(move) { 800 | var next; 801 | next = freeStates.pop(); 802 | next.parent = this; 803 | next.lastMove = move; 804 | next.depth = this.depth + 1; 805 | next.URFtoDLF = this.move('URFtoDLF', this.URFtoDLF, move); 806 | next.FRtoBR = this.move('FRtoBR', this.FRtoBR, move); 807 | next.parity = this.move('parity', this.parity, move); 808 | next.URtoDF = this.move('URtoDF', this.URtoDF, move); 809 | return next; 810 | } 811 | 812 | }; 813 | solution = null; 814 | phase1search = function(state) { 815 | var depth, m, ref, results; 816 | results = []; 817 | for (depth = m = 1, ref = maxDepth; (1 <= ref ? m <= ref : m >= ref); depth = 1 <= ref ? ++m : --m) { 818 | phase1(state, depth); 819 | if (solution !== null) { 820 | break; 821 | } else { 822 | results.push(void 0); 823 | } 824 | } 825 | return results; 826 | }; 827 | phase1 = function(state, depth) { 828 | var len, m, move, next, ref, ref1, results; 829 | if (depth === 0) { 830 | if (state.minDist1() === 0) { 831 | // Make sure we don't start phase 2 with a phase 2 move as the 832 | // last move in phase 1, because phase 2 would then repeat the 833 | // same move. 834 | if (state.lastMove === null || (ref = state.lastMove, indexOf.call(allMoves2, ref) < 0)) { 835 | return phase2search(state); 836 | } 837 | } 838 | } else if (depth > 0) { 839 | if (state.minDist1() <= depth) { 840 | ref1 = state.moves1(); 841 | results = []; 842 | for (m = 0, len = ref1.length; m < len; m++) { 843 | move = ref1[m]; 844 | next = state.next1(move); 845 | phase1(next, depth - 1); 846 | freeStates.push(next); 847 | if (solution !== null) { 848 | break; 849 | } else { 850 | results.push(void 0); 851 | } 852 | } 853 | return results; 854 | } 855 | } 856 | }; 857 | phase2search = function(state) { 858 | var depth, m, ref, results; 859 | // Initialize phase 2 coordinates 860 | state.init2(); 861 | results = []; 862 | for (depth = m = 1, ref = maxDepth - state.depth; (1 <= ref ? m <= ref : m >= ref); depth = 1 <= ref ? ++m : --m) { 863 | phase2(state, depth); 864 | if (solution !== null) { 865 | break; 866 | } else { 867 | results.push(void 0); 868 | } 869 | } 870 | return results; 871 | }; 872 | phase2 = function(state, depth) { 873 | var len, m, move, next, ref, results; 874 | if (depth === 0) { 875 | if (state.minDist2() === 0) { 876 | return solution = state.solution(); 877 | } 878 | } else if (depth > 0) { 879 | if (state.minDist2() <= depth) { 880 | ref = state.moves2(); 881 | results = []; 882 | for (m = 0, len = ref.length; m < len; m++) { 883 | move = ref[m]; 884 | next = state.next2(move); 885 | phase2(next, depth - 1); 886 | freeStates.push(next); 887 | if (solution !== null) { 888 | break; 889 | } else { 890 | results.push(void 0); 891 | } 892 | } 893 | return results; 894 | } 895 | } 896 | }; 897 | freeStates = (function() { 898 | var m, ref, results; 899 | results = []; 900 | for (x = m = 0, ref = maxDepth + 1; (0 <= ref ? m <= ref : m >= ref); x = 0 <= ref ? ++m : --m) { 901 | results.push(new State()); 902 | } 903 | return results; 904 | })(); 905 | state = freeStates.pop().init(this); 906 | phase1search(state); 907 | freeStates.push(state); 908 | if (solution == null) { 909 | return null; 910 | } 911 | // Trim the trailing space and return 912 | return solution.trim(); 913 | }; 914 | 915 | faceNums = { 916 | U: 0, 917 | R: 1, 918 | F: 2, 919 | D: 3, 920 | L: 4, 921 | B: 5 922 | }; 923 | 924 | faceNames = { 925 | 0: 'U', 926 | 1: 'R', 927 | 2: 'F', 928 | 3: 'D', 929 | 4: 'L', 930 | 5: 'B' 931 | }; 932 | 933 | Cube.prototype.solve = function(maxDepth = 22) { 934 | var clone, len, m, move, ref, rotation, solution, upright, uprightSolution; 935 | clone = this.clone(); 936 | upright = clone.upright(); 937 | clone.move(upright); 938 | rotation = new Cube().move(upright).center; 939 | uprightSolution = clone.solveUpright(maxDepth); 940 | if (uprightSolution == null) { 941 | return null; 942 | } 943 | solution = []; 944 | ref = uprightSolution.split(' '); 945 | for (m = 0, len = ref.length; m < len; m++) { 946 | move = ref[m]; 947 | solution.push(faceNames[rotation[faceNums[move[0]]]]); 948 | if (move.length > 1) { 949 | solution[solution.length - 1] += move[1]; 950 | } 951 | } 952 | return solution.join(' '); 953 | }; 954 | 955 | Cube.scramble = function() { 956 | return Cube.inverse(Cube.random().solve()); 957 | }; 958 | 959 | }).call(this); 960 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var init, initialized, solve; 3 | 4 | importScripts('cube.js', 'solve.js'); 5 | 6 | initialized = false; 7 | 8 | init = function() { 9 | if (initialized) { 10 | return; 11 | } 12 | Cube.initSolver(); 13 | return initialized = true; 14 | }; 15 | 16 | solve = function(args) { 17 | var cube; 18 | if (!initialized) { 19 | return; 20 | } 21 | if (args.scramble) { 22 | cube = new Cube(); 23 | cube.move(args.scramble); 24 | } else if (args.cube) { 25 | cube = new Cube(args.cube); 26 | } 27 | return cube.solve(); 28 | }; 29 | 30 | self.onmessage = function(event) { 31 | var args; 32 | args = event.data; 33 | switch (args.cmd) { 34 | case 'init': 35 | init(); 36 | return self.postMessage({ 37 | cmd: 'init', 38 | status: 'ok' 39 | }); 40 | case 'solve': 41 | return self.postMessage({ 42 | cmd: 'solve', 43 | algorithm: solve(args) 44 | }); 45 | } 46 | }; 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cubejs", 3 | "version": "1.3.2", 4 | "description": "JavaScript library for modeling and solving the 3x3x3 Rubik's Cube", 5 | "author": { 6 | "name": "akheron", 7 | "url": "http://www.digip.org/about/" 8 | }, 9 | "maintainers": [ 10 | { 11 | "name": "ldez", 12 | "url": "https://twitter.com/ludnadez" 13 | } 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/ldez/cubejs#readme", 17 | "bugs": { 18 | "url": "https://github.com/ldez/cubejs/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/ldez/cubejs.git" 23 | }, 24 | "keywords": [ 25 | "rubik's cube", 26 | "3x3", 27 | "scrambler", 28 | "solver" 29 | ], 30 | "main": "index.js", 31 | "scripts": { 32 | "build": "gulp", 33 | "test": "gulp test", 34 | "test:auto": "gulp test:auto", 35 | "demo:build": "gulp demo:build", 36 | "prepublish": "gulp build" 37 | }, 38 | "devDependencies": { 39 | "coffeelint": "^2.1.0", 40 | "coffeescript": "^2.7.0", 41 | "del": "^6.0.0", 42 | "fancy-log": "^1.3.2", 43 | "gulp": "^4.0.0", 44 | "gulp-coffee": "^3.0.2", 45 | "gulp-coffeelint": "^0.6.0", 46 | "jasmine-core": "^3.2.1", 47 | "karma": "^6.3.16", 48 | "karma-coffee-preprocessor": "^1.0.1", 49 | "karma-jasmine": "^4.0.1", 50 | "karma-firefox-launcher": "^2.1.3" 51 | }, 52 | "dependencies": { 53 | "npm": "^7.15.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spec/cube.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Cube', -> 2 | 3 | it 'should serialize a cube to string for a default cube', -> 4 | cube = new Cube 5 | expect(cube.asString()).toBe 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB' 6 | 7 | it 'should initiate a cube when provide a String', -> 8 | cube = Cube.fromString 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB' 9 | expect(cube.asString()).toBe 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB' 10 | 11 | it 'should serialize a cube to JSON for a default cube', -> 12 | cube = new Cube 13 | 14 | expectedJSON = 15 | center: [0, 1, 2, 3, 4, 5], 16 | cp: [ 0, 1, 2, 3, 4, 5, 6, 7 ], 17 | co: [ 0, 0, 0, 0, 0, 0, 0, 0 ], 18 | ep: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ], 19 | eo: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 20 | 21 | expect(cube.toJSON()).toEqual expectedJSON 22 | 23 | it 'should rotate U face when move U', -> 24 | cube = new Cube 25 | cube.move 'U' 26 | expect(cube.asString()).toBe 'UUUUUUUUUBBBRRRRRRRRRFFFFFFDDDDDDDDDFFFLLLLLLLLLBBBBBB' 27 | 28 | it 'should rotate cuve face when apply a moves sequence', -> 29 | cube = new Cube 30 | cube.move "U R F' L'" 31 | expect(cube.asString()).toBe 'DURRUFRRRBRBDRBDRBFDDDFFDFFBLLBDBLDLFUUFLLFLLULRUBUUBU' 32 | 33 | it 'should rotate cuve face when apply a moves sequence includes additional notation', -> 34 | cube = new Cube 35 | cube.move "M' u2 z' S" 36 | expect(cube.asString()).toBe 'LLRUFULLRDLDBLBDRDBBFUUDBBFRRLDBDRRLURUFRFULUBFFUDDBFF' 37 | 38 | it 'should resets the cube to the identity cube', -> 39 | cube = new Cube 40 | cube.move "U R F' L'" 41 | cube.identity() 42 | expect(cube.asString()).toBe 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB' 43 | 44 | it 'should return true when the cube is solved (default cube)', -> 45 | cube = new Cube 46 | expect(cube.isSolved()).toBe true 47 | 48 | it 'should return false when the cube is not solved (random cube), and runs without errors in normal time', -> 49 | cube = Cube.random() 50 | expect(cube.isSolved()).toBe false 51 | 52 | it 'should return inverse moves', -> 53 | moves = Cube.inverse "F B' R" 54 | expect(moves).toBe "R' B F'" 55 | 56 | # ignore because Travis is slow 57 | xit 'should solve a solved cube :) ', -> 58 | Cube.initSolver() 59 | cube = new Cube 60 | expect(cube.solve()).toBe "R L U2 R L F2 R2 U2 R2 F2 R2 U2 F2 L2" 61 | 62 | # ignore because Travis is slow 63 | xit 'should return null if no solution is found (maxDepth too low)', -> 64 | Cube.initSolver() 65 | cube = Cube.random() 66 | expect(cube.solve(1)).toBe null 67 | -------------------------------------------------------------------------------- /src/async.coffee: -------------------------------------------------------------------------------- 1 | Cube = @Cube or require('./cube') 2 | 3 | Extend = 4 | asyncOK: !!window.Worker 5 | 6 | _asyncSetup: (workerURI) -> 7 | return if @_worker 8 | @_worker = new window.Worker(workerURI) 9 | @_worker.addEventListener('message', (e) => @_asyncEvent(e)) 10 | @_asyncCallbacks = {} 11 | 12 | _asyncEvent: (e) -> 13 | callbacks = @_asyncCallbacks[e.data.cmd] 14 | return unless callbacks and callbacks.length 15 | callback = callbacks[0] 16 | callbacks.splice(0, 1) 17 | callback(e.data) 18 | 19 | _asyncCallback: (cmd, callback) -> 20 | @_asyncCallbacks[cmd] or= [] 21 | @_asyncCallbacks[cmd].push(callback) 22 | 23 | asyncInit: (workerURI, callback) -> 24 | @_asyncSetup(workerURI) 25 | @_asyncCallback('init', -> callback()) 26 | @_worker.postMessage(cmd: 'init') 27 | 28 | _asyncSolve: (cube, callback) -> 29 | @_asyncSetup() 30 | @_asyncCallback('solve', (data) -> callback(data.algorithm)) 31 | @_worker.postMessage(cmd: 'solve', cube: cube.toJSON()) 32 | 33 | asyncScramble: (callback) -> 34 | @_asyncSetup() 35 | @_asyncCallback('solve', (data) -> callback(Cube.inverse(data.algorithm))) 36 | @_worker.postMessage(cmd: 'solve', cube: Cube.random().toJSON()) 37 | 38 | asyncSolve: (callback) -> 39 | Cube._asyncSolve(this, callback) 40 | 41 | 42 | for key, value of Extend 43 | Cube[key] = value 44 | -------------------------------------------------------------------------------- /src/cube.coffee: -------------------------------------------------------------------------------- 1 | # Centers 2 | [U, R, F, D, L, B] = [0..5] 3 | 4 | # Corners 5 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0..7] 6 | 7 | # Edges 8 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0..11] 9 | 10 | [centerFacelet, cornerFacelet, edgeFacelet] = do -> 11 | _U = (x) -> x - 1 12 | _R = (x) -> _U(9) + x 13 | _F = (x) -> _R(9) + x 14 | _D = (x) -> _F(9) + x 15 | _L = (x) -> _D(9) + x 16 | _B = (x) -> _L(9) + x 17 | [ 18 | # Centers 19 | [4, 13, 22, 31, 40, 49], 20 | # Corners 21 | [ 22 | [_U(9), _R(1), _F(3)], [_U(7), _F(1), _L(3)], 23 | [_U(1), _L(1), _B(3)], [_U(3), _B(1), _R(3)], 24 | [_D(3), _F(9), _R(7)], [_D(1), _L(9), _F(7)], 25 | [_D(7), _B(9), _L(7)], [_D(9), _R(9), _B(7)], 26 | ], 27 | # Edges 28 | [ 29 | [_U(6), _R(2)], [_U(8), _F(2)], [_U(4), _L(2)], [_U(2), _B(2)], 30 | [_D(6), _R(8)], [_D(2), _F(8)], [_D(4), _L(8)], [_D(8), _B(8)], 31 | [_F(6), _R(4)], [_F(4), _L(6)], [_B(6), _L(4)], [_B(4), _R(6)], 32 | ], 33 | ] 34 | 35 | centerColor = ['U', 'R', 'F', 'D', 'L', 'B'] 36 | 37 | cornerColor = [ 38 | ['U', 'R', 'F'], ['U', 'F', 'L'], ['U', 'L', 'B'], ['U', 'B', 'R'], 39 | ['D', 'F', 'R'], ['D', 'L', 'F'], ['D', 'B', 'L'], ['D', 'R', 'B'], 40 | ] 41 | 42 | edgeColor = [ 43 | ['U', 'R'], ['U', 'F'], ['U', 'L'], ['U', 'B'], ['D', 'R'], ['D', 'F'], 44 | ['D', 'L'], ['D', 'B'], ['F', 'R'], ['F', 'L'], ['B', 'L'], ['B', 'R'], 45 | ] 46 | 47 | class Cube 48 | constructor: (other) -> 49 | if other? 50 | @init other 51 | else 52 | @identity() 53 | 54 | # For moves to avoid allocating new objects each time 55 | @newCenter = (0 for x in [0..5]) 56 | @newCp = (0 for x in [0..7]) 57 | @newEp = (0 for x in [0..11]) 58 | @newCo = (0 for x in [0..7]) 59 | @newEo = (0 for x in [0..11]) 60 | 61 | init: (state) -> 62 | @center = state.center.slice 0 63 | @co = state.co.slice 0 64 | @ep = state.ep.slice 0 65 | @cp = state.cp.slice 0 66 | @eo = state.eo.slice 0 67 | 68 | identity: -> 69 | # Initialize to the identity cube 70 | @center = [0..5] 71 | @cp = [0..7] 72 | @co = (0 for x in [0..7]) 73 | @ep = [0..11] 74 | @eo = (0 for x in [0..11]) 75 | 76 | toJSON: -> 77 | center: @center 78 | cp: @cp 79 | co: @co 80 | ep: @ep 81 | eo: @eo 82 | 83 | asString: -> 84 | result = [] 85 | 86 | for i in [0..5] 87 | result[9 * i + 4] = centerColor[@center[i]] 88 | 89 | for i in [0..7] 90 | corner = @cp[i] 91 | ori = @co[i] 92 | for n in [0..2] 93 | result[cornerFacelet[i][(n + ori) % 3]] = cornerColor[corner][n] 94 | 95 | for i in [0..11] 96 | edge = @ep[i] 97 | ori = @eo[i] 98 | for n in [0..1] 99 | result[edgeFacelet[i][(n + ori) % 2]] = edgeColor[edge][n] 100 | 101 | result.join('') 102 | 103 | @fromString: (str) -> 104 | cube = new Cube 105 | 106 | for i in [0..5] 107 | for j in [0..5] 108 | if str[9 * i + 4] is centerColor[j] 109 | cube.center[i] = j 110 | 111 | for i in [0..7] 112 | for ori in [0..2] 113 | break if str[cornerFacelet[i][ori]] in ['U', 'D'] 114 | col1 = str[cornerFacelet[i][(ori + 1) % 3]] 115 | col2 = str[cornerFacelet[i][(ori + 2) % 3]] 116 | 117 | for j in [0..7] 118 | if col1 is cornerColor[j][1] and col2 is cornerColor[j][2] 119 | cube.cp[i] = j 120 | cube.co[i] = ori % 3 121 | 122 | for i in [0..11] 123 | for j in [0..11] 124 | if (str[edgeFacelet[i][0]] is edgeColor[j][0] and 125 | str[edgeFacelet[i][1]] is edgeColor[j][1]) 126 | cube.ep[i] = j 127 | cube.eo[i] = 0 128 | break 129 | if (str[edgeFacelet[i][0]] is edgeColor[j][1] and 130 | str[edgeFacelet[i][1]] is edgeColor[j][0]) 131 | cube.ep[i] = j 132 | cube.eo[i] = 1 133 | break 134 | 135 | cube 136 | 137 | clone: -> 138 | new Cube @toJSON() 139 | 140 | randomize: do -> 141 | randint = (min, max) -> 142 | min + Math.floor(Math.random() * (max - min + 1) ) 143 | 144 | # Fisher-Yates shuffle adapted from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array 145 | shuffle = (array) -> 146 | currentIndex = array.length 147 | 148 | # While there remain elements to shuffle... 149 | while currentIndex isnt 0 150 | # Pick a remaining element... 151 | randomIndex = randint(0, currentIndex - 1) 152 | currentIndex -= 1 153 | 154 | # And swap it with the current element. 155 | temporaryValue = array[currentIndex] 156 | [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]] 157 | return 158 | 159 | getNumSwaps = (arr) -> 160 | numSwaps = 0 161 | seen = (false for x in [0..arr.length - 1]) 162 | # We compute the cycle decomposition 163 | loop 164 | cur = -1 165 | for i in [0..arr.length - 1] 166 | if not seen[i] 167 | cur = i 168 | break 169 | if cur is -1 170 | break 171 | cycleLength = 0 172 | while not seen[cur] 173 | seen[cur] = true 174 | cycleLength++ 175 | cur = arr[cur] 176 | # A cycle is equivalent to cycleLength + 1 swaps 177 | numSwaps += cycleLength + 1 178 | numSwaps 179 | 180 | arePermutationsValid = (cp, ep) -> 181 | numSwaps = getNumSwaps(ep) + getNumSwaps(cp) 182 | numSwaps % 2 is 0 183 | 184 | generateValidRandomPermutation = (cp, ep) -> 185 | # Each shuffle only takes around 12 operations and there's a 50% 186 | # chance of a valid permutation so it'll finish in very good time 187 | shuffle(ep) 188 | shuffle(cp) 189 | until arePermutationsValid(cp, ep) 190 | shuffle(ep) 191 | shuffle(cp) 192 | return 193 | 194 | randomizeOrientation = (arr, numOrientations) -> 195 | ori = 0 196 | for i in [0..arr.length - 1] 197 | ori += (arr[i] = randint(0, numOrientations - 1)) 198 | return 199 | 200 | isOrientationValid = (arr, numOrientations) -> 201 | arr.reduce((a, b) -> a + b) % numOrientations is 0 202 | 203 | generateValidRandomOrientation = (co, eo) -> 204 | # There is a 1/2 and 1/3 probably respectively of each of these 205 | # succeeding so the probability of them running 10 times before 206 | # success is already only 1% and only gets exponentially lower 207 | # and each generation is only in the 10s of operations which is nothing 208 | randomizeOrientation(co, 3) 209 | until isOrientationValid(co, 3) 210 | randomizeOrientation(co, 3) 211 | 212 | randomizeOrientation(eo, 2) 213 | until isOrientationValid(eo, 2) 214 | randomizeOrientation(eo, 2) 215 | 216 | return 217 | 218 | result = -> 219 | generateValidRandomPermutation(@cp, @ep) 220 | generateValidRandomOrientation(@co, @eo) 221 | this 222 | 223 | result 224 | 225 | # A class method returning a new random cube 226 | @random: -> 227 | new Cube().randomize() 228 | 229 | isSolved: -> 230 | clone = @clone() 231 | clone.move clone.upright() 232 | 233 | for cent in [0..5] 234 | return false if clone.center[cent] isnt cent 235 | 236 | for c in [0..7] 237 | return false if clone.cp[c] isnt c 238 | return false if clone.co[c] isnt 0 239 | 240 | for e in [0..11] 241 | return false if clone.ep[e] isnt e 242 | return false if clone.eo[e] isnt 0 243 | 244 | true 245 | 246 | # Multiply this Cube with another Cube, restricted to centers. 247 | centerMultiply: (other) -> 248 | for to in [0..5] 249 | from = other.center[to] 250 | @newCenter[to] = @center[from] 251 | 252 | [@center, @newCenter] = [@newCenter, @center] 253 | this 254 | 255 | # Multiply this Cube with another Cube, restricted to corners. 256 | cornerMultiply: (other) -> 257 | for to in [0..7] 258 | from = other.cp[to] 259 | @newCp[to] = @cp[from] 260 | @newCo[to] = (@co[from] + other.co[to]) % 3 261 | 262 | [@cp, @newCp] = [@newCp, @cp] 263 | [@co, @newCo] = [@newCo, @co] 264 | this 265 | 266 | # Multiply this Cube with another Cube, restricted to edges 267 | edgeMultiply: (other) -> 268 | for to in [0..11] 269 | from = other.ep[to] 270 | @newEp[to] = @ep[from] 271 | @newEo[to] = (@eo[from] + other.eo[to]) % 2 272 | 273 | [@ep, @newEp] = [@newEp, @ep] 274 | [@eo, @newEo] = [@newEo, @eo] 275 | this 276 | 277 | # Multiply this cube with another Cube 278 | multiply: (other) -> 279 | @centerMultiply(other) 280 | @cornerMultiply(other) 281 | @edgeMultiply(other) 282 | this 283 | 284 | @moves: [ 285 | # U 286 | { 287 | center: [0..5] 288 | cp: [UBR, URF, UFL, ULB, DFR, DLF, DBL, DRB] 289 | co: [0, 0, 0, 0, 0, 0, 0, 0] 290 | ep: [UB, UR, UF, UL, DR, DF, DL, DB, FR, FL, BL, BR] 291 | eo: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 292 | } 293 | 294 | # R 295 | { 296 | center: [0..5] 297 | cp: [DFR, UFL, ULB, URF, DRB, DLF, DBL, UBR] 298 | co: [2, 0, 0, 1, 1, 0, 0, 2] 299 | ep: [FR, UF, UL, UB, BR, DF, DL, DB, DR, FL, BL, UR] 300 | eo: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 301 | } 302 | 303 | # F 304 | { 305 | center: [0..5] 306 | cp: [UFL, DLF, ULB, UBR, URF, DFR, DBL, DRB] 307 | co: [1, 2, 0, 0, 2, 1, 0, 0] 308 | ep: [UR, FL, UL, UB, DR, FR, DL, DB, UF, DF, BL, BR] 309 | eo: [0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0] 310 | } 311 | 312 | # D 313 | { 314 | center: [0..5] 315 | cp: [URF, UFL, ULB, UBR, DLF, DBL, DRB, DFR] 316 | co: [0, 0, 0, 0, 0, 0, 0, 0] 317 | ep: [UR, UF, UL, UB, DF, DL, DB, DR, FR, FL, BL, BR] 318 | eo: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 319 | } 320 | 321 | # L 322 | { 323 | center: [0..5] 324 | cp: [URF, ULB, DBL, UBR, DFR, UFL, DLF, DRB] 325 | co: [0, 1, 2, 0, 0, 2, 1, 0] 326 | ep: [UR, UF, BL, UB, DR, DF, FL, DB, FR, UL, DL, BR] 327 | eo: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 328 | } 329 | 330 | # B 331 | { 332 | center: [0..5] 333 | cp: [URF, UFL, UBR, DRB, DFR, DLF, ULB, DBL] 334 | co: [0, 0, 1, 2, 0, 0, 2, 1] 335 | ep: [UR, UF, UL, BR, DR, DF, DL, BL, FR, FL, UB, DB] 336 | eo: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1] 337 | } 338 | 339 | # E 340 | { 341 | center: [U, F, L, D, B, R] 342 | cp: [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] 343 | co: [0, 0, 0, 0, 0, 0, 0, 0] 344 | ep: [UR, UF, UL, UB, DR, DF, DL, DB, FL, BL, BR, FR] 345 | eo: [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1] 346 | } 347 | 348 | # M 349 | { 350 | center: [B, R, U, F, L, D] 351 | cp: [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] 352 | co: [0, 0, 0, 0, 0, 0, 0, 0] 353 | ep: [UR, UB, UL, DB, DR, UF, DL, DF, FR, FL, BL, BR] 354 | eo: [0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0] 355 | } 356 | 357 | # S 358 | { 359 | center: [L, U, F, R, D, B] 360 | cp: [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] 361 | co: [0, 0, 0, 0, 0, 0, 0, 0] 362 | ep: [UL, UF, DL, UB, UR, DF, DR, DB, FR, FL, BL, BR] 363 | eo: [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0] 364 | } 365 | ] 366 | 367 | faceNums = 368 | U: 0 369 | R: 1 370 | F: 2 371 | D: 3 372 | L: 4 373 | B: 5 374 | E: 6 375 | M: 7 376 | S: 8 377 | x: 9 378 | y: 10 379 | z: 11 380 | u: 12 381 | r: 13 382 | f: 14 383 | d: 15 384 | l: 16 385 | b: 17 386 | 387 | faceNames = 388 | 0: 'U' 389 | 1: 'R' 390 | 2: 'F' 391 | 3: 'D' 392 | 4: 'L' 393 | 5: 'B' 394 | 6: 'E' 395 | 7: 'M' 396 | 8: 'S' 397 | 9: 'x' 398 | 10: 'y' 399 | 11: 'z' 400 | 12: 'u' 401 | 13: 'r' 402 | 14: 'f' 403 | 15: 'd' 404 | 16: 'l' 405 | 17: 'b' 406 | 407 | parseAlg = (arg) -> 408 | if typeof arg is 'string' 409 | # String 410 | for part in arg.split(/\s+/) 411 | if part.length is 0 412 | # First and last can be empty 413 | continue 414 | 415 | if part.length > 2 416 | throw new Error "Invalid move: #{part}" 417 | 418 | move = faceNums[part[0]] 419 | if move is undefined 420 | throw new Error "Invalid move: #{part}" 421 | 422 | if part.length is 1 423 | power = 0 424 | else 425 | if part[1] is '2' 426 | power = 1 427 | else if part[1] is "'" 428 | power = 2 429 | else 430 | throw new Error "Invalid move: #{part}" 431 | 432 | move * 3 + power 433 | 434 | else if arg.length? 435 | # Already an array 436 | arg 437 | 438 | else 439 | # A single move 440 | [arg] 441 | 442 | move: (arg) -> 443 | for move in parseAlg(arg) 444 | face = move / 3 | 0 445 | power = move % 3 446 | @multiply(Cube.moves[face]) for x in [0..power] 447 | 448 | this 449 | 450 | upright: -> 451 | clone = @clone() 452 | result = [] 453 | for i in [0..5] 454 | break if clone.center[i] is F 455 | switch i 456 | when D then result.push "x" 457 | when U then result.push "x'" 458 | when B then result.push "x2" 459 | when R then result.push "y" 460 | when L then result.push "y'" 461 | if result.length then clone.move result[0] 462 | for j in [0..5] 463 | break if clone.center[j] is U 464 | switch j 465 | when L then result.push "z" 466 | when R then result.push "z'" 467 | when D then result.push "z2" 468 | result.join ' ' 469 | 470 | @inverse: (arg) -> 471 | result = for move in parseAlg(arg) 472 | face = move / 3 | 0 473 | power = move % 3 474 | face * 3 + -(power - 1) + 1 475 | 476 | result.reverse() 477 | 478 | if typeof arg is 'string' 479 | str = '' 480 | for move in result 481 | face = move / 3 | 0 482 | power = move % 3 483 | str += faceNames[face] 484 | if power is 1 485 | str += '2' 486 | else if power is 2 487 | str += "'" 488 | str += ' ' 489 | str.substring(0, str.length - 1) 490 | 491 | else if arg.length? 492 | result 493 | 494 | else 495 | result[0] 496 | 497 | # x 498 | Cube.moves.push new Cube().move("R M' L'").toJSON() 499 | 500 | # y 501 | Cube.moves.push new Cube().move("U E' D'").toJSON() 502 | 503 | # z 504 | Cube.moves.push new Cube().move("F S B'").toJSON() 505 | 506 | # u 507 | Cube.moves.push new Cube().move("U E'").toJSON() 508 | 509 | # r 510 | Cube.moves.push new Cube().move("R M'").toJSON() 511 | 512 | # f 513 | Cube.moves.push new Cube().move("F S").toJSON() 514 | 515 | # d 516 | Cube.moves.push new Cube().move("D E").toJSON() 517 | 518 | # l 519 | Cube.moves.push new Cube().move("L M").toJSON() 520 | 521 | # b 522 | Cube.moves.push new Cube().move("B S'").toJSON() 523 | 524 | ## Globals 525 | 526 | if module? 527 | module.exports = Cube 528 | else 529 | @Cube = Cube 530 | -------------------------------------------------------------------------------- /src/solve.coffee: -------------------------------------------------------------------------------- 1 | Cube = @Cube or require('./cube') 2 | 3 | # Centers 4 | [U, R, F, D, L, B] = [0..5] 5 | 6 | # Corners 7 | [URF, UFL, ULB, UBR, DFR, DLF, DBL, DRB] = [0..7] 8 | 9 | # Edges 10 | [UR, UF, UL, UB, DR, DF, DL, DB, FR, FL, BL, BR] = [0..11] 11 | 12 | 13 | ## Helpers 14 | 15 | # n choose k, i.e. the binomial coeffiecient 16 | Cnk = (n, k) -> 17 | return 0 if n < k 18 | 19 | if k > n / 2 20 | k = n - k 21 | 22 | s = 1 23 | i = n 24 | j = 1 25 | while i isnt n - k 26 | s *= i 27 | s /= j 28 | i-- 29 | j++ 30 | s 31 | 32 | # n! 33 | factorial = (n) -> 34 | f = 1 35 | for i in [2..n] 36 | f *= i 37 | f 38 | 39 | # Maximum of two values 40 | max = (a, b) -> 41 | if a > b then a else b 42 | 43 | # Rotate elements between l and r left by one place 44 | rotateLeft = (array, l, r) -> 45 | tmp = array[l] 46 | array[i] = array[i + 1] for i in [l..r - 1] 47 | array[r] = tmp 48 | 49 | # Rotate elements between l and r right by one place 50 | rotateRight = (array, l, r) -> 51 | tmp = array[r] 52 | array[i] = array[i - 1] for i in [r..l + 1] 53 | array[l] = tmp 54 | 55 | 56 | # Generate a function that computes permutation indices. 57 | # 58 | # The permutation index actually encodes two indices: Combination, 59 | # i.e. positions of the cubies start..end (A) and their respective 60 | # permutation (B). The maximum value for B is 61 | # 62 | # maxB = (end - start + 1)! 63 | # 64 | # and the index is A * maxB + B 65 | 66 | permutationIndex = (context, start, end, fromEnd=false) -> 67 | maxOur = end - start 68 | maxB = factorial(maxOur + 1) 69 | 70 | if context is 'corners' 71 | maxAll = 7 72 | permName = 'cp' 73 | else 74 | maxAll = 11 75 | permName = 'ep' 76 | 77 | our = (0 for i in [0..maxOur]) 78 | 79 | (index) -> 80 | if index? 81 | # Reset our to [start..end] 82 | our[i] = i + start for i in [0..maxOur] 83 | 84 | b = index % maxB # permutation 85 | a = index / maxB | 0 # combination 86 | 87 | # Invalidate all edges 88 | perm = @[permName] 89 | perm[i] = -1 for i in [0..maxAll] 90 | 91 | # Generate permutation from index b 92 | for j in [1..maxOur] 93 | k = b % (j + 1) 94 | b = b / (j + 1) | 0 95 | # TODO: Implement rotateRightBy(our, 0, j, k) 96 | while k > 0 97 | rotateRight(our, 0, j) 98 | k-- 99 | 100 | # Generate combination and set our edges 101 | x = maxOur 102 | if fromEnd 103 | for j in [0..maxAll] 104 | c = Cnk(maxAll - j, x + 1) 105 | if a - c >= 0 106 | perm[j] = our[maxOur - x] 107 | a -= c 108 | x-- 109 | else 110 | for j in [maxAll..0] 111 | c = Cnk(j, x + 1) 112 | if a - c >= 0 113 | perm[j] = our[x] 114 | a -= c 115 | x-- 116 | 117 | this 118 | 119 | else 120 | perm = @[permName] 121 | our[i] = -1 for i in [0..maxOur] 122 | a = b = x = 0 123 | 124 | # Compute the index a < ((maxAll + 1) choose (maxOur + 1)) and 125 | # the permutation 126 | if fromEnd 127 | for j in [maxAll..0] 128 | if start <= perm[j] <= end 129 | a += Cnk(maxAll - j, x + 1) 130 | our[maxOur - x] = perm[j] 131 | x++ 132 | else 133 | for j in [0..maxAll] 134 | if start <= perm[j] <= end 135 | a += Cnk(j, x + 1) 136 | our[x] = perm[j] 137 | x++ 138 | 139 | # Compute the index b < (maxOur + 1)! for the permutation 140 | for j in [maxOur..0] 141 | k = 0 142 | while our[j] isnt start + j 143 | rotateLeft(our, 0, j) 144 | k++ 145 | b = (j + 1) * b + k 146 | 147 | a * maxB + b 148 | 149 | 150 | Include = 151 | # The twist of the 8 corners, 0 <= twist < 3^7. The orientation of 152 | # the DRB corner is fully determined by the orientation of the other 153 | # corners. 154 | twist: (twist) -> 155 | if twist? 156 | parity = 0 157 | for i in [6..0] 158 | ori = twist % 3 159 | twist = (twist / 3) | 0 160 | 161 | @co[i] = ori 162 | parity += ori 163 | 164 | @co[7] = ((3 - parity % 3) % 3) 165 | this 166 | 167 | else 168 | v = 0 169 | for i in [0..6] 170 | v = 3 * v + @co[i] 171 | v 172 | 173 | # The flip of the 12 edges, 0 <= flip < 2^11. The orientation of the 174 | # BR edge is fully determined by the orientation of the other edges. 175 | flip: (flip) -> 176 | if flip? 177 | parity = 0 178 | for i in [10..0] 179 | ori = flip % 2 180 | flip = flip / 2 | 0 181 | 182 | @eo[i] = ori 183 | parity += ori 184 | 185 | @eo[11] = ((2 - parity % 2) % 2) 186 | this 187 | 188 | else 189 | v = 0 190 | for i in [0..10] 191 | v = 2 * v + @eo[i] 192 | v 193 | 194 | # Parity of the corner permutation 195 | cornerParity: -> 196 | s = 0 197 | for i in [DRB..URF + 1] 198 | for j in [i - 1..URF] 199 | s++ if @cp[j] > @cp[i] 200 | 201 | s % 2 202 | 203 | # Parity of the edges permutation. Parity of corners and edges are 204 | # the same if the cube is solvable. 205 | edgeParity: -> 206 | s = 0 207 | for i in [BR..UR + 1] 208 | for j in [i - 1..UR] 209 | s++ if @ep[j] > @ep[i] 210 | 211 | s % 2 212 | 213 | # Permutation of the six corners URF, UFL, ULB, UBR, DFR, DLF 214 | URFtoDLF: permutationIndex('corners', URF, DLF) 215 | 216 | # Permutation of the three edges UR, UF, UL 217 | URtoUL: permutationIndex('edges', UR, UL) 218 | 219 | # Permutation of the three edges UB, DR, DF 220 | UBtoDF: permutationIndex('edges', UB, DF) 221 | 222 | # Permutation of the six edges UR, UF, UL, UB, DR, DF 223 | URtoDF: permutationIndex('edges', UR, DF) 224 | 225 | # Permutation of the equator slice edges FR, FL, BL and BR 226 | FRtoBR: permutationIndex('edges', FR, BR, true) 227 | 228 | 229 | for key, value of Include 230 | Cube::[key] = value 231 | 232 | 233 | computeMoveTable = (context, coord, size) -> 234 | # Loop through all valid values for the coordinate, setting cube's 235 | # state in each iteration. Then apply each of the 18 moves to the 236 | # cube, and compute the resulting coordinate. 237 | apply = if context is 'corners' then 'cornerMultiply' else 'edgeMultiply' 238 | 239 | cube = new Cube 240 | 241 | for i in [0..size-1] 242 | cube[coord](i) 243 | inner = [] 244 | for j in [0..5] 245 | move = Cube.moves[j] 246 | for k in [0..2] 247 | cube[apply](move) 248 | inner.push(cube[coord]()) 249 | # 4th face turn restores the cube 250 | cube[apply](move) 251 | inner 252 | 253 | # Because we only have the phase 2 URtoDF coordinates, we need to 254 | # merge the URtoUL and UBtoDF coordinates to URtoDF in the beginning 255 | # of phase 2. 256 | mergeURtoDF = do -> 257 | a = new Cube 258 | b = new Cube 259 | 260 | (URtoUL, UBtoDF) -> 261 | # Collisions can be found because unset are set to -1 262 | a.URtoUL(URtoUL) 263 | b.UBtoDF(UBtoDF) 264 | 265 | for i in [0..7] 266 | if a.ep[i] isnt -1 267 | if b.ep[i] isnt -1 268 | return -1 # collision 269 | else 270 | b.ep[i] = a.ep[i] 271 | 272 | b.URtoDF() 273 | 274 | N_TWIST = 2187 # 3^7 corner orientations 275 | N_FLIP = 2048 # 2^11 possible edge flips 276 | N_PARITY = 2 # 2 possible parities 277 | 278 | N_FRtoBR = 11880 # 12!/(12-4)! permutations of FR..BR edges 279 | N_SLICE1 = 495 # (12 choose 4) possible positions of FR..BR edges 280 | N_SLICE2 = 24 # 4! permutations of FR..BR edges in phase 2 281 | 282 | N_URFtoDLF = 20160 # 8!/(8-6)! permutations of URF..DLF corners 283 | 284 | # The URtoDF move table is only computed for phase 2 because the full 285 | # table would have >650000 entries 286 | N_URtoDF = 20160 # 8!/(8-6)! permutation of UR..DF edges in phase 2 287 | 288 | N_URtoUL = 1320 # 12!/(12-3)! permutations of UR..UL edges 289 | N_UBtoDF = 1320 # 12!/(12-3)! permutations of UB..DF edges 290 | 291 | # The move table for parity is so small that it's included here 292 | Cube.moveTables = 293 | parity: [ 294 | [1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], 295 | [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0], 296 | ] 297 | twist: null 298 | flip: null 299 | FRtoBR: null 300 | URFtoDLF: null 301 | URtoDF: null 302 | URtoUL: null 303 | UBtoDF: null 304 | mergeURtoDF: null 305 | 306 | # Other move tables are computed on the fly 307 | moveTableParams = 308 | # name: [scope, size] 309 | twist: ['corners', N_TWIST] 310 | flip: ['edges', N_FLIP] 311 | FRtoBR: ['edges', N_FRtoBR] 312 | URFtoDLF: ['corners', N_URFtoDLF] 313 | URtoDF: ['edges', N_URtoDF] 314 | URtoUL: ['edges', N_URtoUL] 315 | UBtoDF: ['edges', N_UBtoDF] 316 | mergeURtoDF: [] # handled specially 317 | 318 | Cube.computeMoveTables = (tables...) -> 319 | if tables.length is 0 320 | tables = (name for name of moveTableParams) 321 | 322 | for tableName in tables 323 | # Already computed 324 | continue if @moveTables[tableName] isnt null 325 | 326 | if tableName is 'mergeURtoDF' 327 | @moveTables.mergeURtoDF = do -> 328 | for URtoUL in [0..335] 329 | for UBtoDF in [0..335] 330 | mergeURtoDF(URtoUL, UBtoDF) 331 | else 332 | [scope, size] = moveTableParams[tableName] 333 | @moveTables[tableName] = computeMoveTable(scope, tableName, size) 334 | 335 | this 336 | 337 | 338 | # Phase 1: All moves are valid 339 | allMoves1 = [0..17] 340 | 341 | # The list of next valid phase 1 moves when the given face was turned 342 | # in the last move 343 | nextMoves1 = do -> 344 | for lastFace in [0..5] 345 | next = [] 346 | # Don't allow commuting moves, e.g. U U'. Also make sure that 347 | # opposite faces are always moved in the same order, i.e. allow 348 | # U D but no D U. This avoids sequences like U D U'. 349 | for face in [0..5] when face isnt lastFace and face isnt lastFace - 3 350 | for power in [0..2] # single, double or inverse move 351 | next.push(face * 3 + power) 352 | next 353 | 354 | # Phase 2: Double moves of all faces plus quarter moves of U and D 355 | allMoves2 = [0, 1, 2, 4, 7, 9, 10, 11, 13, 16] 356 | 357 | nextMoves2 = do -> 358 | for lastFace in [0..5] 359 | next = [] 360 | for face in [0..5] when face isnt lastFace and face isnt lastFace - 3 361 | # Allow all moves of U and D and double moves of others 362 | powers = if face in [0, 3] then [0..2] else [1] 363 | for power in powers 364 | next.push(face * 3 + power) 365 | next 366 | 367 | # 8 values are encoded in one number 368 | pruning = (table, index, value) -> 369 | pos = index % 8 370 | slot = index >> 3 371 | shift = pos << 2 372 | 373 | if value? 374 | # Set 375 | table[slot] &= ~(0xF << shift) 376 | table[slot] |= (value << shift) 377 | value 378 | else 379 | # Get 380 | (table[slot] & (0xF << shift)) >>> shift 381 | 382 | computePruningTable = (phase, size, currentCoords, nextIndex) -> 383 | # Initialize all values to 0xF 384 | table = (0xFFFFFFFF for x in [0..Math.ceil(size / 8) - 1]) 385 | 386 | if phase is 1 387 | moves = allMoves1 388 | else 389 | moves = allMoves2 390 | 391 | depth = 0 392 | pruning(table, 0, depth) 393 | done = 1 394 | 395 | # In each iteration, take each state found in the previous depth and 396 | # compute the next state. Stop when all states have been assigned a 397 | # depth. 398 | while done isnt size 399 | for index in [0..size - 1] when pruning(table, index) is depth 400 | current = currentCoords(index) 401 | for move in moves 402 | next = nextIndex(current, move) 403 | if pruning(table, next) is 0xF 404 | pruning(table, next, depth + 1) 405 | done++ 406 | depth++ 407 | 408 | table 409 | 410 | Cube.pruningTables = 411 | sliceTwist: null 412 | sliceFlip: null 413 | sliceURFtoDLFParity: null 414 | sliceURtoDFParity: null 415 | 416 | pruningTableParams = 417 | # name: [phase, size, currentCoords, nextIndex] 418 | sliceTwist: [ 419 | 1, 420 | N_SLICE1 * N_TWIST, 421 | (index) -> [index % N_SLICE1, index / N_SLICE1 | 0], 422 | (current, move) -> 423 | [slice, twist] = current 424 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0 425 | newTwist = Cube.moveTables.twist[twist][move] 426 | newTwist * N_SLICE1 + newSlice 427 | ] 428 | sliceFlip: [ 429 | 1 430 | N_SLICE1 * N_FLIP 431 | (index) -> [index % N_SLICE1, index / N_SLICE1 | 0], 432 | (current, move) -> 433 | [slice, flip] = current 434 | newSlice = Cube.moveTables.FRtoBR[slice * 24][move] / 24 | 0 435 | newFlip = Cube.moveTables.flip[flip][move] 436 | newFlip * N_SLICE1 + newSlice 437 | ] 438 | sliceURFtoDLFParity: [ 439 | 2, 440 | N_SLICE2 * N_URFtoDLF * N_PARITY, 441 | (index) -> 442 | [index % 2, (index / 2 | 0) % N_SLICE2, (index / 2 | 0) / N_SLICE2 | 0] 443 | (current, move) -> 444 | [parity, slice, URFtoDLF] = current 445 | newParity = Cube.moveTables.parity[parity][move] 446 | newSlice = Cube.moveTables.FRtoBR[slice][move] 447 | newURFtoDLF = Cube.moveTables.URFtoDLF[URFtoDLF][move] 448 | (newURFtoDLF * N_SLICE2 + newSlice) * 2 + newParity 449 | ] 450 | sliceURtoDFParity: [ 451 | 2, 452 | N_SLICE2 * N_URtoDF * N_PARITY, 453 | (index) -> 454 | [index % 2, (index / 2 | 0) % N_SLICE2, (index / 2 | 0) / N_SLICE2 | 0] 455 | (current, move) -> 456 | [parity, slice, URtoDF] = current 457 | newParity = Cube.moveTables.parity[parity][move] 458 | newSlice = Cube.moveTables.FRtoBR[slice][move] 459 | newURtoDF = Cube.moveTables.URtoDF[URtoDF][move] 460 | (newURtoDF * N_SLICE2 + newSlice) * 2 + newParity 461 | ] 462 | 463 | Cube.computePruningTables = (tables...) -> 464 | if tables.length is 0 465 | tables = (name for name of pruningTableParams) 466 | 467 | for tableName in tables 468 | # Already computed 469 | continue if @pruningTables[tableName] isnt null 470 | 471 | params = pruningTableParams[tableName] 472 | @pruningTables[tableName] = computePruningTable(params...) 473 | 474 | this 475 | 476 | Cube.initSolver = -> 477 | Cube.computeMoveTables() 478 | Cube.computePruningTables() 479 | 480 | Cube::solveUpright = (maxDepth=22) -> 481 | # Names for all moves, i.e. U, U2, U', F, F2, ... 482 | moveNames = do -> 483 | faceName = ['U', 'R', 'F', 'D', 'L', 'B'] 484 | powerName = ['', '2', "'"] 485 | 486 | result = [] 487 | for face in [0..5] 488 | for power in [0..2] 489 | result.push(faceName[face] + powerName[power]) 490 | 491 | result 492 | 493 | class State 494 | constructor: (cube) -> 495 | @parent = null 496 | @lastMove = null 497 | @depth = 0 498 | 499 | @init(cube) if cube 500 | 501 | init: (cube) -> 502 | # Phase 1 coordinates 503 | @flip = cube.flip() 504 | @twist = cube.twist() 505 | @slice = cube.FRtoBR() / N_SLICE2 | 0 506 | 507 | # Phase 2 coordinates 508 | @parity = cube.cornerParity() 509 | @URFtoDLF = cube.URFtoDLF() 510 | @FRtoBR = cube.FRtoBR() 511 | 512 | # These are later merged to URtoDF when phase 2 begins 513 | @URtoUL = cube.URtoUL() 514 | @UBtoDF = cube.UBtoDF() 515 | 516 | this 517 | 518 | solution: -> 519 | if @parent 520 | @parent.solution() + moveNames[@lastMove] + ' ' 521 | else 522 | '' 523 | 524 | ## Helpers 525 | 526 | move: (table, index, move) -> 527 | Cube.moveTables[table][index][move] 528 | 529 | pruning: (table, index) -> 530 | pruning(Cube.pruningTables[table], index) 531 | 532 | ## Phase 1 533 | 534 | # Return the next valid phase 1 moves for this state 535 | moves1: -> 536 | if @lastMove isnt null then nextMoves1[@lastMove / 3 | 0] else allMoves1 537 | 538 | # Compute the minimum number of moves to the end of phase 1 539 | minDist1: -> 540 | # The maximum number of moves to the end of phase 1 wrt. the 541 | # combination flip and slice coordinates only 542 | d1 = @pruning('sliceFlip', N_SLICE1 * @flip + @slice) 543 | 544 | # The combination of twist and slice coordinates 545 | d2 = @pruning('sliceTwist', N_SLICE1 * @twist + @slice) 546 | 547 | # The true minimal distance is the maximum of these two 548 | max(d1, d2) 549 | 550 | # Compute the next phase 1 state for the given move 551 | next1: (move) -> 552 | next = freeStates.pop() 553 | next.parent = this 554 | next.lastMove = move 555 | next.depth = @depth + 1 556 | 557 | next.flip = @move('flip', @flip, move) 558 | next.twist = @move('twist', @twist, move) 559 | next.slice = @move('FRtoBR', @slice * 24, move) / 24 | 0 560 | 561 | next 562 | 563 | 564 | ## Phase 2 565 | 566 | # Return the next valid phase 2 moves for this state 567 | moves2: -> 568 | if @lastMove isnt null then nextMoves2[@lastMove / 3 | 0] else allMoves2 569 | 570 | # Compute the minimum number of moves to the solved cube 571 | minDist2: -> 572 | index1 = (N_SLICE2 * @URtoDF + @FRtoBR) * N_PARITY + @parity 573 | d1 = @pruning('sliceURtoDFParity', index1) 574 | 575 | index2 = (N_SLICE2 * @URFtoDLF + @FRtoBR) * N_PARITY + @parity 576 | d2 = @pruning('sliceURFtoDLFParity', index2) 577 | 578 | max(d1, d2) 579 | 580 | # Initialize phase 2 coordinates 581 | init2: (top=true) -> 582 | if @parent is null 583 | # Already assigned for the initial state 584 | return 585 | 586 | # For other states, the phase 2 state is computed based on 587 | # parent's state. 588 | @parent.init2(false) 589 | 590 | @URFtoDLF = @move('URFtoDLF', @parent.URFtoDLF, @lastMove) 591 | @FRtoBR = @move('FRtoBR', @parent.FRtoBR, @lastMove) 592 | @parity = @move('parity', @parent.parity, @lastMove) 593 | @URtoUL = @move('URtoUL', @parent.URtoUL, @lastMove) 594 | @UBtoDF = @move('UBtoDF', @parent.UBtoDF, @lastMove) 595 | 596 | if top 597 | # This is the initial phase 2 state. Get the URtoDF coordinate 598 | # by merging URtoUL and UBtoDF 599 | @URtoDF = @move('mergeURtoDF', @URtoUL, @UBtoDF) 600 | 601 | # Compute the next phase 2 state for the given move 602 | next2: (move) -> 603 | next = freeStates.pop() 604 | next.parent = this 605 | next.lastMove = move 606 | next.depth = @depth + 1 607 | 608 | next.URFtoDLF = @move('URFtoDLF', @URFtoDLF, move) 609 | next.FRtoBR = @move('FRtoBR', @FRtoBR, move) 610 | next.parity = @move('parity', @parity, move) 611 | next.URtoDF = @move('URtoDF', @URtoDF, move) 612 | 613 | next 614 | 615 | 616 | solution = null 617 | 618 | phase1search = (state) -> 619 | for depth in [1..maxDepth] 620 | phase1(state, depth) 621 | break if solution isnt null 622 | 623 | phase1 = (state, depth) -> 624 | if depth is 0 625 | if state.minDist1() is 0 626 | # Make sure we don't start phase 2 with a phase 2 move as the 627 | # last move in phase 1, because phase 2 would then repeat the 628 | # same move. 629 | if state.lastMove is null or state.lastMove not in allMoves2 630 | phase2search(state) 631 | 632 | else if depth > 0 633 | if state.minDist1() <= depth 634 | for move in state.moves1() 635 | next = state.next1(move) 636 | phase1(next, depth - 1) 637 | freeStates.push(next) 638 | break if solution isnt null 639 | 640 | phase2search = (state) -> 641 | # Initialize phase 2 coordinates 642 | state.init2() 643 | 644 | for depth in [1..maxDepth - state.depth] 645 | phase2(state, depth) 646 | break if solution isnt null 647 | 648 | phase2 = (state, depth) -> 649 | if depth is 0 650 | if state.minDist2() is 0 651 | solution = state.solution() 652 | else if depth > 0 653 | if state.minDist2() <= depth 654 | for move in state.moves2() 655 | next = state.next2(move) 656 | phase2(next, depth - 1) 657 | freeStates.push(next) 658 | break if solution isnt null 659 | 660 | freeStates = (new State for x in [0..maxDepth + 1]) 661 | state = freeStates.pop().init(this) 662 | phase1search(state) 663 | freeStates.push(state) 664 | 665 | return null if not solution? 666 | # Trim the trailing space and return 667 | solution.trim() 668 | 669 | faceNums = 670 | U: 0 671 | R: 1 672 | F: 2 673 | D: 3 674 | L: 4 675 | B: 5 676 | 677 | faceNames = 678 | 0: 'U' 679 | 1: 'R' 680 | 2: 'F' 681 | 3: 'D' 682 | 4: 'L' 683 | 5: 'B' 684 | 685 | Cube::solve = (maxDepth=22) -> 686 | clone = @clone() 687 | upright = clone.upright() 688 | clone.move upright 689 | rotation = new Cube().move(upright).center 690 | uprightSolution = clone.solveUpright maxDepth 691 | return null if not uprightSolution? 692 | solution = [] 693 | for move in uprightSolution.split ' ' 694 | solution.push faceNames[rotation[faceNums[move[0]]]] 695 | if move.length > 1 696 | solution[solution.length - 1] += move[1] 697 | solution.join ' ' 698 | 699 | Cube.scramble = -> 700 | Cube.inverse(Cube.random().solve()) 701 | -------------------------------------------------------------------------------- /src/worker.coffee: -------------------------------------------------------------------------------- 1 | importScripts('cube.js', 'solve.js') 2 | 3 | initialized = false 4 | 5 | init = -> 6 | return if initialized 7 | Cube.initSolver() 8 | initialized = true 9 | 10 | 11 | solve = (args) -> 12 | return unless initialized 13 | 14 | if args.scramble 15 | cube = new Cube 16 | cube.move(args.scramble) 17 | else if args.cube 18 | cube = new Cube(args.cube) 19 | 20 | cube.solve() 21 | 22 | 23 | self.onmessage = (event) -> 24 | args = event.data 25 | 26 | switch args.cmd 27 | when 'init' 28 | init() 29 | self.postMessage(cmd: 'init', status: 'ok') 30 | 31 | when 'solve' 32 | self.postMessage(cmd: 'solve', algorithm: solve(args)) 33 | --------------------------------------------------------------------------------