├── .flake8 ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── noxfile.py ├── poetry.lock ├── pyproject.toml └── twophase ├── __init__.py ├── cubes ├── __init__.py ├── coordcube.py ├── cubiecube.py └── facecube.py ├── pieces.py ├── random.py ├── solve.py └── tables.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E501, W503 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: "ubuntu-latest" 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install nox 23 | - name: Lint source 24 | if: matrix.python-version == 3.8 25 | run: | 26 | nox -s lint 27 | - name: Check and build 28 | run: | 29 | nox -s build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # python artifacts 2 | __pycache__ 3 | *.egg-info 4 | 5 | # build and test artifacts 6 | .nox 7 | dist 8 | 9 | # move and pruning tables 10 | tables.json 11 | # legacy, code no longer generates this 12 | tables.pkl 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Tom Begley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cube-solver 2 | 3 | A pure Python implementation of Herbert Kociemba's two-phase algorithm for solving the Rubik's Cube 4 | 5 | ## Installation 6 | 7 | Requires Python 3. Install with 8 | 9 | ```sh 10 | pip install git+https://github.com/tcbegley/cube-solver.git 11 | ``` 12 | 13 | Note that depending on how your system is configured, you may need to replace `pip` with `pip3` in the above command to install for Python 3. 14 | 15 | ## Usage 16 | 17 | To solve a cube, just import the `solve` method and pass a cube string. 18 | 19 | ```python 20 | from twophase import solve 21 | 22 | solve("") 23 | ``` 24 | 25 | Where the cube string is a 54 character string, consisting of the characters U, R, F, D, L, B (corresponding to the Upper, Right, Front, Down, Left and Back faces). Each character corresponds to one of the 54 stickers on the cube: 26 | 27 | ```plaintext 28 | |------------| 29 | |-U1--U2--U3-| 30 | |------------| 31 | |-U4--U5--U6-| 32 | |------------| 33 | |-U7--U8--U9-| 34 | |------------|------------|------------|------------| 35 | |-L1--L2--L3-|-F1--F2--F3-|-R1--R2--R3-|-B1--B2--B3-| 36 | |------------|------------|------------|------------| 37 | |-L4--L5--L6-|-F4--F5--F6-|-R4--R5--R6-|-B4--B5--B6-| 38 | |------------|------------|------------|------------| 39 | |-L7--L8--L9-|-F7--F8--F9-|-R7--R8--R9-|-B7--B8--B9-| 40 | |------------|------------|------------|------------| 41 | |-D1--D2--D3-| 42 | |------------| 43 | |-D4--D5--D6-| 44 | |------------| 45 | |-D7--D8--D9-| 46 | |------------| 47 | ``` 48 | 49 | and should be specified in the order U1-U9, R1-R9, F1-F9, D1-D9, L1-L9, B1-B9. 50 | 51 | For example, a completely solved cube is represented by the string `"UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"`. 52 | 53 | `solve` will return a solution unless timeout has been reached (default is 10 seconds). Typically it will find a solution very quickly unless you set a low upper bound on the number of moves allowed. Note that the first time you run `solve`, it will precompute move tables needed for the solution which might take ~1 minute. Subsequent runs will be much faster. 54 | 55 | If you want to keep searching for better solutions, use the `solve_best` or 56 | `solve_best_generator` functions. `solve_best` reduces `max_length` each time a 57 | solution is found and continues searching for a better solution. All solutions 58 | found are returned in a list at the end. `solve_best_generator` creates a 59 | generator that yields solutions as they are found. 60 | 61 | ```python 62 | from twophase import solve_best, solve_best_generator 63 | 64 | # returns a list of solutions 65 | solve_best("") 66 | 67 | # creates a generator that yields solutions as they are found 68 | solve_best_generator("") 69 | ``` 70 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | SOURCES = ["twophase", "noxfile.py"] 4 | 5 | 6 | @nox.session() 7 | def lint(session): 8 | """Lint python source""" 9 | session.install("black", "isort", "flake8") 10 | session.run("black", "--check", *SOURCES) 11 | session.run("isort", "--check", "--recursive", *SOURCES) 12 | session.run("flake8", *SOURCES) 13 | 14 | 15 | @nox.session() 16 | def build(session): 17 | """Check and build""" 18 | session.install("poetry") 19 | env = {"VIRTUAL_ENV": session.virtualenv.location} 20 | session.run("poetry", "check", env=env, external=True) 21 | session.run("poetry", "build", env=env, external=True) 22 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | package = [] 2 | 3 | [metadata] 4 | content-hash = "de7600acb56ad9a67b77fd08bb5acd96ae59d0bbc6d538e5682444b7d4b1cc16" 5 | python-versions = "^3.6" 6 | 7 | [metadata.files] 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "poetry>=0.12",] 3 | build-backend = "poetry.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "twophase" 7 | version = "0.1.0" 8 | description = "Rubik's cube solver implementing Kociemba's two-phase algorithm." 9 | authors = [ "tcbegley ",] 10 | license = "MIT" 11 | classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License",] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.6" 15 | 16 | [tool.black] 17 | line-length = 79 18 | target-version = [ "py36" ] 19 | 20 | [tool.isort] 21 | include_trailing_comma = true 22 | line_length = 79 23 | multi_line_output = 3 24 | -------------------------------------------------------------------------------- /twophase/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .solve import SolutionManager 4 | 5 | 6 | def solve(cube_string, max_length=25, max_time=10): 7 | """ 8 | Solve the cube specified by cube_string, return the first solution found 9 | as long as max_time not exceeded. 10 | """ 11 | sm = SolutionManager(cube_string) 12 | solution = sm.solve(max_length, time.time() + max_time) 13 | if isinstance(solution, str): 14 | return solution 15 | elif solution == -2: 16 | raise RuntimeError("max_time exceeded, no solution found") 17 | elif solution == -1: 18 | raise RuntimeError("no solution found, try increasing max_length") 19 | raise RuntimeError( 20 | f"SolutionManager.solve: unexpected return value {solution}" 21 | ) 22 | 23 | 24 | def solve_best(cube_string, max_length=25, max_time=10): 25 | """ 26 | Solve the cube repeatedly, reducing max_length each time a solution is 27 | found until timeout is reached or no more solutions are found. 28 | 29 | Returns all solutions found as a list. 30 | """ 31 | return list(solve_best_generator(cube_string, max_length, max_time)) 32 | 33 | 34 | def solve_best_generator(cube_string, max_length=25, max_time=10): 35 | """ 36 | Solve the cube repeatedly, reducing max_length each time a solution is 37 | found until timeout is reached or no more solutions are found. 38 | 39 | Yields the solution each time it is found. 40 | """ 41 | sm = SolutionManager(cube_string) 42 | timeout = time.time() + max_time 43 | while True: 44 | solution = sm.solve(max_length, timeout) 45 | 46 | if isinstance(solution, str): 47 | yield solution 48 | max_length = len(solution.split(" ")) - 1 49 | elif solution == -2 or solution == -1: 50 | # timeout or no more solutions 51 | break 52 | else: 53 | raise RuntimeError( 54 | f"SolutionManager.solve: unexpected return value {solution}" 55 | ) 56 | -------------------------------------------------------------------------------- /twophase/cubes/__init__.py: -------------------------------------------------------------------------------- 1 | from .coordcube import CoordCube 2 | from .cubiecube import CubieCube 3 | from .facecube import FaceCube 4 | 5 | __all__ = ["CoordCube", "CubieCube", "FaceCube"] 6 | -------------------------------------------------------------------------------- /twophase/cubes/coordcube.py: -------------------------------------------------------------------------------- 1 | from ..tables import Tables 2 | from .cubiecube import CubieCube 3 | 4 | 5 | class CoordCube: 6 | """ 7 | Coordinate representation of cube. Updates coordinates using pre-computed 8 | move tables. 9 | """ 10 | 11 | def __init__(self, twist=0, flip=0, udslice=0, edge4=0, edge8=0, corner=0): 12 | self.tables = Tables() 13 | 14 | # initialise from cubiecube c 15 | self.twist = twist 16 | self.flip = flip 17 | self.udslice = udslice 18 | self.edge4 = edge4 19 | self.edge8 = edge8 20 | self.corner = corner 21 | 22 | @classmethod 23 | def from_cubiecube(cls, cube): 24 | """ 25 | Create a CoordCube from an existing CubieCube. 26 | """ 27 | if not isinstance(cube, CubieCube): 28 | raise TypeError("Expected argument of type CubieCube") 29 | return cls( 30 | cube.twist, 31 | cube.flip, 32 | cube.udslice, 33 | cube.edge4, 34 | cube.edge8, 35 | cube.corner, 36 | ) 37 | 38 | def move(self, mv): 39 | """ 40 | Update all coordinates after applying move mv using the move tables. 41 | 42 | Parameters 43 | ---------- 44 | mv : int 45 | Integer representing one of 18 non-identity face turns. Calulate as 46 | 3 * i + j where i = 0, 1, 2, 3, 4, 5 for U, R, F, D, L, B 47 | respectively, and j = 0, 1, 2 for quarter turn clockwise, half turn 48 | and quarter turn anticlockwise respectively. 49 | """ 50 | self.twist = self.tables.twist_move[self.twist][mv] 51 | self.flip = self.tables.flip_move[self.flip][mv] 52 | self.udslice = self.tables.udslice_move[self.udslice][mv] 53 | self.edge4 = self.tables.edge4_move[self.edge4][mv] 54 | self.edge8 = self.tables.edge8_move[self.edge8][mv] 55 | self.corner = self.tables.corner_move[self.corner][mv] 56 | -------------------------------------------------------------------------------- /twophase/cubes/cubiecube.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class describes cubes on the level of the cubies. 3 | """ 4 | from functools import reduce 5 | 6 | from ..pieces import Corner, Edge 7 | from . import facecube 8 | 9 | 10 | def choose(n, k): 11 | """ 12 | A fast way to compute binomial coefficients by Andrew Dalke. 13 | """ 14 | if 0 <= k <= n: 15 | num = 1 16 | den = 1 17 | for i in range(1, min(k, n - k) + 1): 18 | num *= n 19 | den *= i 20 | n -= 1 21 | return num // den 22 | else: 23 | return 0 24 | 25 | 26 | # Moves on the cubie level, gives permutation and orientation after the moves 27 | # U, R, F, D, L, B resp from a clean cube. This will be used to compute move 28 | # tables and the composition rules. 29 | _cpU = ( 30 | Corner.UBR, 31 | Corner.URF, 32 | Corner.UFL, 33 | Corner.ULB, 34 | Corner.DFR, 35 | Corner.DLF, 36 | Corner.DBL, 37 | Corner.DRB, 38 | ) 39 | _coU = (0, 0, 0, 0, 0, 0, 0, 0) 40 | _epU = ( 41 | Edge.UB, 42 | Edge.UR, 43 | Edge.UF, 44 | Edge.UL, 45 | Edge.DR, 46 | Edge.DF, 47 | Edge.DL, 48 | Edge.DB, 49 | Edge.FR, 50 | Edge.FL, 51 | Edge.BL, 52 | Edge.BR, 53 | ) 54 | _eoU = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 55 | 56 | _cpR = ( 57 | Corner.DFR, 58 | Corner.UFL, 59 | Corner.ULB, 60 | Corner.URF, 61 | Corner.DRB, 62 | Corner.DLF, 63 | Corner.DBL, 64 | Corner.UBR, 65 | ) 66 | _coR = (2, 0, 0, 1, 1, 0, 0, 2) 67 | _epR = ( 68 | Edge.FR, 69 | Edge.UF, 70 | Edge.UL, 71 | Edge.UB, 72 | Edge.BR, 73 | Edge.DF, 74 | Edge.DL, 75 | Edge.DB, 76 | Edge.DR, 77 | Edge.FL, 78 | Edge.BL, 79 | Edge.UR, 80 | ) 81 | _eoR = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 82 | 83 | _cpF = ( 84 | Corner.UFL, 85 | Corner.DLF, 86 | Corner.ULB, 87 | Corner.UBR, 88 | Corner.URF, 89 | Corner.DFR, 90 | Corner.DBL, 91 | Corner.DRB, 92 | ) 93 | _coF = (1, 2, 0, 0, 2, 1, 0, 0) 94 | _epF = ( 95 | Edge.UR, 96 | Edge.FL, 97 | Edge.UL, 98 | Edge.UB, 99 | Edge.DR, 100 | Edge.FR, 101 | Edge.DL, 102 | Edge.DB, 103 | Edge.UF, 104 | Edge.DF, 105 | Edge.BL, 106 | Edge.BR, 107 | ) 108 | _eoF = (0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0) 109 | 110 | _cpD = ( 111 | Corner.URF, 112 | Corner.UFL, 113 | Corner.ULB, 114 | Corner.UBR, 115 | Corner.DLF, 116 | Corner.DBL, 117 | Corner.DRB, 118 | Corner.DFR, 119 | ) 120 | _coD = (0, 0, 0, 0, 0, 0, 0, 0) 121 | _epD = ( 122 | Edge.UR, 123 | Edge.UF, 124 | Edge.UL, 125 | Edge.UB, 126 | Edge.DF, 127 | Edge.DL, 128 | Edge.DB, 129 | Edge.DR, 130 | Edge.FR, 131 | Edge.FL, 132 | Edge.BL, 133 | Edge.BR, 134 | ) 135 | _eoD = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 136 | 137 | _cpL = ( 138 | Corner.URF, 139 | Corner.ULB, 140 | Corner.DBL, 141 | Corner.UBR, 142 | Corner.DFR, 143 | Corner.UFL, 144 | Corner.DLF, 145 | Corner.DRB, 146 | ) 147 | _coL = (0, 1, 2, 0, 0, 2, 1, 0) 148 | _epL = ( 149 | Edge.UR, 150 | Edge.UF, 151 | Edge.BL, 152 | Edge.UB, 153 | Edge.DR, 154 | Edge.DF, 155 | Edge.FL, 156 | Edge.DB, 157 | Edge.FR, 158 | Edge.UL, 159 | Edge.DL, 160 | Edge.BR, 161 | ) 162 | _eoL = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 163 | 164 | _cpB = ( 165 | Corner.URF, 166 | Corner.UFL, 167 | Corner.UBR, 168 | Corner.DRB, 169 | Corner.DFR, 170 | Corner.DLF, 171 | Corner.ULB, 172 | Corner.DBL, 173 | ) 174 | _coB = (0, 0, 1, 2, 0, 0, 2, 1) 175 | _epB = ( 176 | Edge.UR, 177 | Edge.UF, 178 | Edge.UL, 179 | Edge.BR, 180 | Edge.DR, 181 | Edge.DF, 182 | Edge.DL, 183 | Edge.BL, 184 | Edge.FR, 185 | Edge.FL, 186 | Edge.UB, 187 | Edge.DB, 188 | ) 189 | _eoB = (0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1) 190 | 191 | 192 | class CubieCube: 193 | def __init__(self, cp=None, co=None, ep=None, eo=None): 194 | if cp and co and ep and eo: 195 | self.cp = cp[:] 196 | self.co = co[:] 197 | self.ep = ep[:] 198 | self.eo = eo[:] 199 | else: 200 | # Initialise clean cube if position not given. 201 | self.cp = [ 202 | Corner.URF, 203 | Corner.UFL, 204 | Corner.ULB, 205 | Corner.UBR, 206 | Corner.DFR, 207 | Corner.DLF, 208 | Corner.DBL, 209 | Corner.DRB, 210 | ] 211 | self.co = [0, 0, 0, 0, 0, 0, 0, 0] 212 | self.ep = [ 213 | Edge.UR, 214 | Edge.UF, 215 | Edge.UL, 216 | Edge.UB, 217 | Edge.DR, 218 | Edge.DF, 219 | Edge.DL, 220 | Edge.DB, 221 | Edge.FR, 222 | Edge.FL, 223 | Edge.BL, 224 | Edge.BR, 225 | ] 226 | self.eo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 227 | 228 | def corner_multiply(self, b): 229 | """ 230 | Compute permutation and orientation of corners after applying 231 | permutation represented by b to current cube. 232 | 233 | Parameters 234 | ---------- 235 | b : CubieCube 236 | Permutation to apply represented as a CubieCube. 237 | 238 | Notes 239 | ----- 240 | We use "is replaced by" notation for the permutations. So b.cp[URF] 241 | gives the index of the piece that replaces URF when applying b. To 242 | explain the rules we use the example of first applying F then applying 243 | R. 244 | 245 | Under F we have URF<-UFL and under R we have UBR<-URF. Hence under FR 246 | we have UBR<-UFL, leading to the rule 247 | 248 | (F*R).cp[UBR] = F.cp[R.cp[UBR]] 249 | 250 | The corner orientation arrays tell us the change in orientation of a 251 | piece (based on where it ends up) due to that move. So for example 252 | F.co[URF] = 1, which means that as UFL moves to URF under the F move, 253 | its orientation increases by 1. To get the total change in orientation 254 | of the piece that moves to UBR under FR we use the rule 255 | 256 | (F*R).co[UBR] = F.co[R.cp[UBR]] + R.co[UBR]. 257 | """ 258 | corner_perm = [self.cp[b.cp[i]] for i in range(8)] 259 | corner_ori = [(self.co[b.cp[i]] + b.co[i]) % 3 for i in range(8)] 260 | self.co = corner_ori[:] 261 | self.cp = corner_perm[:] 262 | 263 | def edge_multiply(self, b): 264 | """ 265 | Compute permutation and orientation of edges after applying permutation 266 | represented by b to current cube. 267 | 268 | Parameters 269 | ---------- 270 | b : CubieCube 271 | Permutation to apply represented as a CubieCube. 272 | 273 | Notes 274 | ----- 275 | See docstring of corner_multiply (which operates analogously to this 276 | method) for a description of how the update rules are derived. 277 | """ 278 | edge_perm = [self.ep[b.ep[i]] for i in range(12)] 279 | edge_ori = [(self.eo[b.ep[i]] + b.eo[i]) % 2 for i in range(12)] 280 | self.eo = edge_ori[:] 281 | self.ep = edge_perm[:] 282 | 283 | def multiply(self, b): 284 | """ 285 | Compute permutation and orientation of edges and corners after applying 286 | permutation represented by b to the current cube. 287 | 288 | Parameters 289 | ---------- 290 | b : CubieCube 291 | Permutation to apply represented as a CubieCube 292 | """ 293 | self.corner_multiply(b) 294 | self.edge_multiply(b) 295 | 296 | def move(self, i): 297 | """ 298 | Helper function for applying one of 6 canonical moves 299 | """ 300 | self.multiply(MOVE_CUBE[i]) 301 | 302 | def inverse_cubiecube(self): 303 | """ 304 | Compute the inverse of the current cube. 305 | """ 306 | cube = CubieCube() 307 | for e in range(12): 308 | cube.ep[self.ep[e]] = e 309 | for e in range(12): 310 | cube.eo[e] = self.eo[cube.ep[e]] 311 | for c in range(8): 312 | cube.cp[self.cp[c]] = c 313 | for c in range(8): 314 | ori = self.co[cube.cp[c]] 315 | cube.co[c] = (-ori) % 3 316 | return cube 317 | 318 | def to_facecube(self): 319 | """ 320 | Convert CubieCube to FaceCube. 321 | """ 322 | ret = facecube.FaceCube() 323 | for i in range(8): 324 | j = self.cp[i] 325 | ori = self.co[i] 326 | for k in range(3): 327 | ret.f[ 328 | facecube.corner_facelet[i][(k + ori) % 3] 329 | ] = facecube.corner_color[j][k] 330 | for i in range(12): 331 | j = self.ep[i] 332 | ori = self.eo[i] 333 | for k in range(2): 334 | facelet_index = facecube.edge_facelet[i][(k + ori) % 2] 335 | ret.f[facelet_index] = facecube.edge_color[j][k] 336 | return ret 337 | 338 | @property 339 | def corner_parity(self): 340 | """ 341 | Corner parity of the CubieCube. Cube is solveable if and only if this 342 | matches the edge parity. 343 | """ 344 | s = 0 345 | for i in range(7, 0, -1): 346 | for j in range(i - 1, -1, -1): 347 | if self.cp[j] > self.cp[i]: 348 | s += 1 349 | return s % 2 350 | 351 | @property 352 | def edge_parity(self): 353 | """ 354 | Edge parity of the CubieCube. Cube is solveable if and only if this 355 | matches the corner parity. 356 | """ 357 | s = 0 358 | for i in range(11, 0, -1): 359 | for j in range(i - 1, -1, -1): 360 | if self.ep[j] > self.ep[i]: 361 | s += 1 362 | return s % 2 363 | 364 | # ---------- Phase 1 Coordinates ---------- # 365 | @property 366 | def twist(self): 367 | """ 368 | Compute twist, the coordinate representing corner orientation. We take 369 | the orientation of the first 7 corners, represented as 0, 1 or 2 as 370 | there are three possibilities, and view that as a ternary number in the 371 | range 0, ..., 3^7 - 1. 372 | 373 | Notes 374 | ----- 375 | The orientation of the first eleven corners determines the orientation 376 | of the last, hence we only include the orientation of the first 7 377 | corners in the calculation of twist. 378 | """ 379 | return reduce(lambda x, y: 3 * x + y, self.co[:7]) 380 | 381 | @twist.setter 382 | def twist(self, twist): 383 | """ 384 | Set the twist of the cube. Each of the values 0, ..., 3^7-1 determines 385 | a distinct way of orienting each of the 8 corners. 386 | 387 | Parameters 388 | ---------- 389 | twist : int 390 | Orientation of the 8 corners encoded as twist coordinate. Must 391 | satisfy 0 <= twist < 3^7. 392 | """ 393 | if not 0 <= twist < 3 ** 7: 394 | raise ValueError( 395 | "{} is out of range for twist, must take values in " 396 | "0, ..., 2186.".format(twist) 397 | ) 398 | total = 0 399 | for i in range(7): 400 | x = twist % 3 401 | self.co[6 - i] = x 402 | total += x 403 | twist //= 3 404 | self.co[7] = (-total) % 3 405 | 406 | @property 407 | def flip(self): 408 | """ 409 | Compute flip, the coordinate representing edge orientation. We take 410 | the orientation of the first 11 edges, represented as 0 or 1 as there 411 | are two possibilities, and view that as a binary number in the range 412 | 0, ..., 2^11 - 1. 413 | 414 | Notes 415 | ----- 416 | The orientation of the first eleven edges determines the orientation 417 | of the last, hence we only include the orientation of the first 11 418 | edges in the calculation of flip. 419 | """ 420 | return reduce(lambda x, y: 2 * x + y, self.eo[:11]) 421 | 422 | @flip.setter 423 | def flip(self, flip): 424 | """ 425 | Set the flip of the cube. Each of the values 0, ..., 2^11-1 determines 426 | a distinct way of orienting each of the 12 edges. 427 | 428 | Parameters 429 | ---------- 430 | flip : int 431 | Orientation of the 12 corners encoded as flip coordinate. Must 432 | satisfy 0 <= flip < 2^11. 433 | """ 434 | if not 0 <= flip < 2 ** 11: 435 | raise ValueError( 436 | "{} is out of range for flip, must take values in " 437 | "0, ..., 2047.".format(flip) 438 | ) 439 | total = 0 440 | for i in range(11): 441 | x = flip % 2 442 | self.eo[10 - i] = x 443 | total += x 444 | flip //= 2 445 | self.eo[11] = (-total) % 2 446 | 447 | @property 448 | def udslice(self): 449 | """ 450 | Compute udslice, the coordinate representing position, but not the 451 | order, of the 4 edges FR, FL, BL, BR. These 4 edges must be in the 452 | middle layer for phase 2 to begin. If they are in the middle layer, 453 | udslice will have the value 0. 454 | 455 | Since there are 12 possible positions and we care only about those 4 456 | edges, udslice takes values in the range 0, ..., 12C4 - 1. 457 | """ 458 | udslice, seen = 0, 0 459 | for j in range(12): 460 | if 8 <= self.ep[j] < 12: 461 | seen += 1 462 | elif seen >= 1: 463 | udslice += choose(j, seen - 1) 464 | return udslice 465 | 466 | @udslice.setter 467 | def udslice(self, udslice): 468 | """ 469 | Set the udslice of the cube. Each of the values 0, ..., 12C4 - 1 470 | determines a distinct set of 4 positions for the edges FR, FL, BL, BR 471 | to occupy. Note that it does not determine the order of these edges. 472 | 473 | Parameters 474 | ---------- 475 | udslice : int 476 | Position of the 4 aforementioned edges encoded as udslice 477 | coordinate. Must satisfy 0 <= slice < 12C4. 478 | """ 479 | if not 0 <= udslice < choose(12, 4): 480 | raise ValueError( 481 | "{} is out of range for udslice, must take values in " 482 | "0, ..., 494.".format(udslice) 483 | ) 484 | udslice_edge = [Edge.FR, Edge.FL, Edge.BL, Edge.BR] 485 | other_edge = [ 486 | Edge.UR, 487 | Edge.UF, 488 | Edge.UL, 489 | Edge.UB, 490 | Edge.DR, 491 | Edge.DF, 492 | Edge.DL, 493 | Edge.DB, 494 | ] 495 | # invalidate edges 496 | for i in range(12): 497 | self.ep[i] = Edge.DB 498 | # we first position the slice edges 499 | seen = 3 500 | for j in range(11, -1, -1): 501 | if udslice - choose(j, seen) < 0: 502 | self.ep[j] = udslice_edge[seen] 503 | seen -= 1 504 | else: 505 | udslice -= choose(j, seen) 506 | # then the remaining edges 507 | x = 0 508 | for j in range(12): 509 | if self.ep[j] == Edge.DB: 510 | self.ep[j] = other_edge[x] 511 | x += 1 512 | 513 | # ---------- Phase 2 Coordinates ---------- # 514 | @property 515 | def edge4(self): 516 | """ 517 | Compute edge4, the coordinate representing permutation of the 4 edges 518 | FR, FL, BL, FR. This assumes that the cube is in phase 2 position, so 519 | in particular the 4 edges are correctly placed, just perhaps not 520 | correctly ordered. edge4 takes values in the range 0, ..., 4! - 1 = 23. 521 | """ 522 | edge4 = self.ep[8:] 523 | ret = 0 524 | for j in range(3, 0, -1): 525 | s = 0 526 | for i in range(j): 527 | if edge4[i] > edge4[j]: 528 | s += 1 529 | ret = j * (ret + s) 530 | return ret 531 | 532 | @edge4.setter 533 | def edge4(self, edge4): 534 | """ 535 | Set the edge4 of the cube. Each of the values 0, ..., 4! - 1 determines 536 | a distinct order of the 4 edges FR, FL, BL, BR in the middle slice 537 | during phase 2. 538 | 539 | Parameters 540 | ---------- 541 | edge4 : int 542 | Order of the 4 aforementioned edges encoded as edge4 coordinate. 543 | Must satisfy 0 <= edge4 < 4! 544 | """ 545 | if not 0 <= edge4 < 24: 546 | raise ValueError( 547 | f"{edge4} is out of range for edge4, must take values in 0-23" 548 | ) 549 | sliceedge = [Edge.FR, Edge.FL, Edge.BL, Edge.BR] 550 | coeffs = [0] * 3 551 | for i in range(1, 4): 552 | coeffs[i - 1] = edge4 % (i + 1) 553 | edge4 //= i + 1 554 | perm = [0] * 4 555 | for i in range(2, -1, -1): 556 | perm[i + 1] = sliceedge.pop(i + 1 - coeffs[i]) 557 | perm[0] = sliceedge[0] 558 | self.ep[8:] = perm[:] 559 | 560 | @property 561 | def edge8(self): 562 | """ 563 | Compute edge8, the coordinate representing permutation of the 8 edges 564 | UR, UF, UL, UB, DR, DF, DL, DB. In phase 2 these edges will all be in 565 | the U and D slices. 566 | 567 | There are 8 possible positions for the 8 edges, so edge8 takes values 568 | in the range 0, ..., 8! - 1. 569 | """ 570 | edge8 = 0 571 | for j in range(7, 0, -1): 572 | s = 0 573 | for i in range(j): 574 | if self.ep[i] > self.ep[j]: 575 | s += 1 576 | edge8 = j * (edge8 + s) 577 | return edge8 578 | 579 | @edge8.setter 580 | def edge8(self, edge8): 581 | """ 582 | Set the edge8 of the cube. Each of the values 0, ..., 8! - 1 determines 583 | a distinct order of the 8 edges UR, UF, UL, UB, DR, DF, DL, DB in the U 584 | and D slices during phase 2. 585 | 586 | Parameters 587 | ---------- 588 | edge8 : int 589 | Order of the 8 aforementioned edges encoded as edge8 coordinate. 590 | Must satisfy 0 <= edge8 < 8! 591 | """ 592 | edges = list(range(8)) 593 | perm = [0] * 8 594 | coeffs = [0] * 7 595 | for i in range(1, 8): 596 | coeffs[i - 1] = edge8 % (i + 1) 597 | edge8 //= i + 1 598 | for i in range(6, -1, -1): 599 | perm[i + 1] = edges.pop(i + 1 - coeffs[i]) 600 | perm[0] = edges[0] 601 | self.ep[:8] = perm[:] 602 | 603 | @property 604 | def corner(self): 605 | """ 606 | Compute corner, the coordinate representing permutation of the 8 607 | corners. 608 | 609 | There are 8 possible positions for the 8 corners, so corner takes 610 | values in the range 0, ..., 8! - 1. 611 | """ 612 | c = 0 613 | for j in range(7, 0, -1): 614 | s = 0 615 | for i in range(j): 616 | if self.cp[i] > self.cp[j]: 617 | s += 1 618 | c = j * (c + s) 619 | return c 620 | 621 | @corner.setter 622 | def corner(self, corn): 623 | """ 624 | Set the corner of the cube. Each of the values 0, ..., 8! - 1 625 | determines a distinct permutation of the 8 corners. 626 | 627 | Parameters 628 | ---------- 629 | corner : int 630 | Order of the 8 corners encoded as corner coordinate. Must satisfy 631 | 0 <= corner < 8! 632 | """ 633 | corners = list(range(8)) 634 | perm = [0] * 8 635 | coeffs = [0] * 7 636 | for i in range(1, 8): 637 | coeffs[i - 1] = corn % (i + 1) 638 | corn //= i + 1 639 | for i in range(6, -1, -1): 640 | perm[i + 1] = corners.pop(i + 1 - coeffs[i]) 641 | perm[0] = corners[0] 642 | self.cp = perm[:] 643 | 644 | # ---------- Misc. Coordinates ---------- # 645 | 646 | # edge permutation coordinate not used in solving, 647 | # but needed to generate random cubes 648 | @property 649 | def edge(self): 650 | """ 651 | Compute edge, the coordinate representing permutation of the 12 652 | corners. 653 | 654 | There are 12 possible positions for the 12 edges, so edge takes values 655 | in the range 0, ..., 12! - 1. 656 | """ 657 | e = 0 658 | for j in range(11, 0, -1): 659 | s = 0 660 | for i in range(j): 661 | if self.ep[i] > self.ep[j]: 662 | s += 1 663 | e = j * (e + s) 664 | return e 665 | 666 | @edge.setter 667 | def edge(self, edge): 668 | """ 669 | Set the edge8 of the cube. Each of the values 0, ..., 8! - 1 determines 670 | a distinct order of the 8 edges UR, UF, UL, UB, DR, DF, DL, DB in the U 671 | and D slices during phase 2. 672 | 673 | Parameters 674 | ---------- 675 | edge8 : int 676 | Order of the 8 aforementioned edges encoded as edge8 coordinate. 677 | Must satisfy 0 <= edge8 < 8! 678 | """ 679 | edges = list(range(12)) 680 | perm = [0] * 12 681 | coeffs = [0] * 11 682 | for i in range(1, 12): 683 | coeffs[i - 1] = edge % (i + 1) 684 | edge //= i + 1 685 | for i in range(10, -1, -1): 686 | perm[i + 1] = edges.pop(i + 1 - coeffs[i]) 687 | perm[0] = edges[0] 688 | self.ep = perm[:] 689 | 690 | # ---------- Solvability Check ---------- # 691 | 692 | # Check a cubiecube for solvability 693 | # 694 | def verify(self): 695 | """ 696 | Check if current cube position is solvable. 697 | 698 | Returns 699 | ------- 700 | int: 701 | Integer encoding solvability of cube. 702 | 0: Solvable 703 | -2: not all 12 edges exist exactly once 704 | -3: flip error: one edge should be flipped 705 | -4: not all corners exist exactly once 706 | -5: twist error - a corner must be twisted 707 | -6: Parity error - two corners or edges have to be exchanged 708 | """ 709 | total = 0 710 | edge_count = [0 for i in range(12)] 711 | for e in range(12): 712 | edge_count[self.ep[e]] += 1 713 | for i in range(12): 714 | if edge_count[i] != 1: 715 | return -2 716 | for i in range(12): 717 | total += self.eo[i] 718 | if total % 2 != 0: 719 | return -3 720 | corner_count = [0] * 8 721 | for c in range(8): 722 | corner_count[self.cp[c]] += 1 723 | for i in range(8): 724 | if corner_count[i] != 1: 725 | return -4 726 | total = 0 727 | for i in range(8): 728 | total += self.co[i] 729 | if total % 3 != 0: 730 | return -5 731 | if self.edge_parity != self.corner_parity: 732 | return -6 733 | return 0 734 | 735 | 736 | # we store the six possible clockwise 1/4 turn moves in the following array. 737 | MOVE_CUBE = [CubieCube() for i in range(6)] 738 | 739 | MOVE_CUBE[0].cp = _cpU 740 | MOVE_CUBE[0].co = _coU 741 | MOVE_CUBE[0].ep = _epU 742 | MOVE_CUBE[0].eo = _eoU 743 | 744 | MOVE_CUBE[1].cp = _cpR 745 | MOVE_CUBE[1].co = _coR 746 | MOVE_CUBE[1].ep = _epR 747 | MOVE_CUBE[1].eo = _eoR 748 | 749 | MOVE_CUBE[2].cp = _cpF 750 | MOVE_CUBE[2].co = _coF 751 | MOVE_CUBE[2].ep = _epF 752 | MOVE_CUBE[2].eo = _eoF 753 | 754 | MOVE_CUBE[3].cp = _cpD 755 | MOVE_CUBE[3].co = _coD 756 | MOVE_CUBE[3].ep = _epD 757 | MOVE_CUBE[3].eo = _eoD 758 | 759 | MOVE_CUBE[4].cp = _cpL 760 | MOVE_CUBE[4].co = _coL 761 | MOVE_CUBE[4].ep = _epL 762 | MOVE_CUBE[4].eo = _eoL 763 | 764 | MOVE_CUBE[5].cp = _cpB 765 | MOVE_CUBE[5].co = _coB 766 | MOVE_CUBE[5].ep = _epB 767 | MOVE_CUBE[5].eo = _eoB 768 | -------------------------------------------------------------------------------- /twophase/cubes/facecube.py: -------------------------------------------------------------------------------- 1 | from ..pieces import Color, Facelet 2 | from . import cubiecube 3 | 4 | # Maps corner positions to facelet positions 5 | corner_facelet = ( 6 | (Facelet.U9, Facelet.R1, Facelet.F3), 7 | (Facelet.U7, Facelet.F1, Facelet.L3), 8 | (Facelet.U1, Facelet.L1, Facelet.B3), 9 | (Facelet.U3, Facelet.B1, Facelet.R3), 10 | (Facelet.D3, Facelet.F9, Facelet.R7), 11 | (Facelet.D1, Facelet.L9, Facelet.F7), 12 | (Facelet.D7, Facelet.B9, Facelet.L7), 13 | (Facelet.D9, Facelet.R9, Facelet.B7), 14 | ) 15 | 16 | # Maps edge positions to facelet positions 17 | edge_facelet = ( 18 | (Facelet.U6, Facelet.R2), 19 | (Facelet.U8, Facelet.F2), 20 | (Facelet.U4, Facelet.L2), 21 | (Facelet.U2, Facelet.B2), 22 | (Facelet.D6, Facelet.R8), 23 | (Facelet.D2, Facelet.F8), 24 | (Facelet.D4, Facelet.L8), 25 | (Facelet.D8, Facelet.B8), 26 | (Facelet.F6, Facelet.R4), 27 | (Facelet.F4, Facelet.L6), 28 | (Facelet.B6, Facelet.L4), 29 | (Facelet.B4, Facelet.R6), 30 | ) 31 | 32 | # Maps corner positions to colours 33 | corner_color = ( 34 | (Color.U, Color.R, Color.F), 35 | (Color.U, Color.F, Color.L), 36 | (Color.U, Color.L, Color.B), 37 | (Color.U, Color.B, Color.R), 38 | (Color.D, Color.F, Color.R), 39 | (Color.D, Color.L, Color.F), 40 | (Color.D, Color.B, Color.L), 41 | (Color.D, Color.R, Color.B), 42 | ) 43 | 44 | # Maps edge positions to colours 45 | edge_color = ( 46 | (Color.U, Color.R), 47 | (Color.U, Color.F), 48 | (Color.U, Color.L), 49 | (Color.U, Color.B), 50 | (Color.D, Color.R), 51 | (Color.D, Color.F), 52 | (Color.D, Color.L), 53 | (Color.D, Color.B), 54 | (Color.F, Color.R), 55 | (Color.F, Color.L), 56 | (Color.B, Color.L), 57 | (Color.B, Color.R), 58 | ) 59 | 60 | 61 | class FaceCube: 62 | def __init__(self, cube_string="".join(c * 9 for c in "URFDLB")): 63 | """ 64 | Initialise FaceCube from cube_string, if cube_string is not provided we 65 | initialise a clean cube. 66 | """ 67 | self.f = [0] * 54 68 | for i in range(54): 69 | self.f[i] = Color[cube_string[i]] 70 | 71 | def to_string(self): 72 | """Convert facecube to cubestring""" 73 | return "".join(Color(i).name for i in self.f) 74 | 75 | def to_cubiecube(self): 76 | """Convert FaceCube to CubieCube""" 77 | cc = cubiecube.CubieCube() 78 | for i in range(8): 79 | # all corner names start with U or D, allowing us to find 80 | # orientation of any given corner as follows 81 | for ori in range(3): 82 | if self.f[corner_facelet[i][ori]] in [Color.U, Color.D]: 83 | break 84 | color1 = self.f[corner_facelet[i][(ori + 1) % 3]] 85 | color2 = self.f[corner_facelet[i][(ori + 2) % 3]] 86 | for j in range(8): 87 | if ( 88 | color1 == corner_color[j][1] 89 | and color2 == corner_color[j][2] 90 | ): 91 | cc.cp[i] = j 92 | cc.co[i] = ori 93 | break 94 | 95 | for i in range(12): 96 | for j in range(12): 97 | if ( 98 | self.f[edge_facelet[i][0]] == edge_color[j][0] 99 | and self.f[edge_facelet[i][1]] == edge_color[j][1] 100 | ): 101 | cc.ep[i] = j 102 | cc.eo[i] = 0 103 | break 104 | if ( 105 | self.f[edge_facelet[i][0]] == edge_color[j][1] 106 | and self.f[edge_facelet[i][1]] == edge_color[j][0] 107 | ): 108 | cc.ep[i] = j 109 | cc.eo[i] = 1 110 | break 111 | return cc 112 | -------------------------------------------------------------------------------- /twophase/pieces.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class Color(IntEnum): 5 | U = 0 6 | R = 1 7 | F = 2 8 | D = 3 9 | L = 4 10 | B = 5 11 | 12 | 13 | class Corner(IntEnum): 14 | URF = 0 15 | UFL = 1 16 | ULB = 2 17 | UBR = 3 18 | DFR = 4 19 | DLF = 5 20 | DBL = 6 21 | DRB = 7 22 | 23 | 24 | class Edge(IntEnum): 25 | UR = 0 26 | UF = 1 27 | UL = 2 28 | UB = 3 29 | DR = 4 30 | DF = 5 31 | DL = 6 32 | DB = 7 33 | FR = 8 34 | FL = 9 35 | BL = 10 36 | BR = 11 37 | 38 | 39 | class Facelet(IntEnum): 40 | U1 = 0 41 | U2 = 1 42 | U3 = 2 43 | U4 = 3 44 | U5 = 4 45 | U6 = 5 46 | U7 = 6 47 | U8 = 7 48 | U9 = 8 49 | R1 = 9 50 | R2 = 10 51 | R3 = 11 52 | R4 = 12 53 | R5 = 13 54 | R6 = 14 55 | R7 = 15 56 | R8 = 16 57 | R9 = 17 58 | F1 = 18 59 | F2 = 19 60 | F3 = 20 61 | F4 = 21 62 | F5 = 22 63 | F6 = 23 64 | F7 = 24 65 | F8 = 25 66 | F9 = 26 67 | D1 = 27 68 | D2 = 28 69 | D3 = 29 70 | D4 = 30 71 | D5 = 31 72 | D6 = 32 73 | D7 = 33 74 | D8 = 34 75 | D9 = 35 76 | L1 = 36 77 | L2 = 37 78 | L3 = 38 79 | L4 = 39 80 | L5 = 40 81 | L6 = 41 82 | L7 = 42 83 | L8 = 43 84 | L9 = 44 85 | B1 = 45 86 | B2 = 46 87 | B3 = 47 88 | B4 = 48 89 | B5 = 49 90 | B6 = 50 91 | B7 = 51 92 | B8 = 52 93 | B9 = 53 94 | -------------------------------------------------------------------------------- /twophase/random.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from .cubes import cubiecube 4 | from .tables import Tables 5 | 6 | 7 | def random_cube(): 8 | cc = cubiecube.CubieCube() 9 | cc.flip = random.randint(0, Tables.FLIP) 10 | cc.twist = random.randint(0, Tables.TWIST) 11 | while True: 12 | cc.corner = random.randint(0, Tables.CORNER) 13 | cc.edge = random.randint(0, Tables.EDGE) 14 | if cc.edge_parity == cc.corner_parity: 15 | break 16 | fc = cc.to_facecube() 17 | return fc.to_string() 18 | -------------------------------------------------------------------------------- /twophase/solve.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .cubes import CoordCube, FaceCube 4 | from .pieces import Color 5 | from .tables import Tables 6 | 7 | 8 | class SolutionManager: 9 | def __init__(self, facelets): 10 | """ 11 | A utility class for managing the search for the solution. 12 | 13 | Parameters 14 | ---------- 15 | facelets: str 16 | Starting position of the cube. Should be a 54 character string 17 | specifying the stickers on each face (in order U R F D L B), 18 | reading row by row from the top left hand corner to the bottom 19 | right 20 | """ 21 | self.tables = Tables() 22 | 23 | self.facelets = facelets.upper() 24 | 25 | status = self.verify() 26 | if status: 27 | error_message = { 28 | -1: "each colour should appear exactly 9 times", 29 | -2: "not all edges exist exactly once", 30 | -3: "one edge should be flipped", 31 | -4: "not all corners exist exactly once", 32 | -5: "one corner should be twisted", 33 | -6: "two corners or edges should be exchanged", 34 | } 35 | raise ValueError("Invalid cube: {}".format(error_message[status])) 36 | 37 | def solve(self, max_length=25, timeout=float("inf")): 38 | """ 39 | Solve the cube. 40 | 41 | This method implements back to back IDA* searches for phase 1 and phase 42 | 2, returning the result. Can be called multiple times with decreasing 43 | max_length to try and find better solutions. 44 | 45 | Parameters 46 | ---------- 47 | max_length: int, optional 48 | Upper bound for the allowed number of moves. 49 | max_time: int or float, optional 50 | Time at which to quit searching. Algorithm will quit when 51 | ``time.time() > max_time``. 52 | """ 53 | # prepare for phase 1 54 | self._phase_1_initialise(max_length) 55 | self._allowed_length = max_length 56 | self._timeout = timeout 57 | 58 | for depth in range(self._allowed_length): 59 | n = self._phase_1_search(0, depth) 60 | if n >= 0: 61 | # solution found 62 | return self._solution_to_string(n) 63 | elif n == -2: 64 | # time limit exceeded 65 | return -2 66 | 67 | # no solution found 68 | return -1 69 | 70 | def verify(self): 71 | count = [0] * 6 72 | try: 73 | for char in self.facelets: 74 | count[Color[char]] += 1 75 | except (IndexError, ValueError): 76 | return -1 77 | for i in range(6): 78 | if count[i] != 9: 79 | return -1 80 | 81 | fc = FaceCube(self.facelets) 82 | cc = fc.to_cubiecube() 83 | 84 | return cc.verify() 85 | 86 | def _phase_1_initialise(self, max_length): 87 | # the lists 'axis' and 'power' will store the nth move (index of face 88 | # being turned stored in axis, number of clockwise quarter turns stored 89 | # in power). The nth move is stored in position n-1 90 | self.axis = [0] * max_length 91 | self.power = [0] * max_length 92 | 93 | # the lists twist, flip and udslice store the phase 1 coordinates after 94 | # n moves. position 0 stores the inital states, the coordinates after n 95 | # moves are stored in position n 96 | self.twist = [0] * max_length 97 | self.flip = [0] * max_length 98 | self.udslice = [0] * max_length 99 | 100 | # similarly to above, these lists store the phase 2 coordinates after n 101 | # moves. 102 | self.corner = [0] * max_length 103 | self.edge4 = [0] * max_length 104 | self.edge8 = [0] * max_length 105 | 106 | # the following two arrays store minimum number of moves required to 107 | # reach phase 2 or a solution respectively 108 | # after n moves. these estimates come from the pruning tables and are 109 | # used to exclude branches in the search tree. 110 | self.min_dist_1 = [0] * max_length 111 | self.min_dist_2 = [0] * max_length 112 | 113 | # initialise the arrays from the input 114 | self.f = FaceCube(self.facelets) 115 | self.c = CoordCube.from_cubiecube(self.f.to_cubiecube()) 116 | self.twist[0] = self.c.twist 117 | self.flip[0] = self.c.flip 118 | self.udslice[0] = self.c.udslice 119 | self.corner[0] = self.c.corner 120 | self.edge4[0] = self.c.edge4 121 | self.edge8[0] = self.c.edge8 122 | self.min_dist_1[0] = self._phase_1_cost(0) 123 | 124 | def _phase_2_initialise(self, n): 125 | if time.time() > self._timeout: 126 | return -2 127 | # initialise phase 2 search from the phase 1 solution 128 | cc = self.f.to_cubiecube() 129 | for i in range(n): 130 | for j in range(self.power[i]): 131 | cc.move(self.axis[i]) 132 | self.edge4[n] = cc.edge4 133 | self.edge8[n] = cc.edge8 134 | self.corner[n] = cc.corner 135 | self.min_dist_2[n] = self._phase_2_cost(n) 136 | for depth in range(self._allowed_length - n): 137 | m = self._phase_2_search(n, depth) 138 | if m >= 0: 139 | return m 140 | return -1 141 | 142 | def _phase_1_cost(self, n): 143 | """ 144 | Cost of current position for use in phase 1. Returns a lower bound on 145 | the number of moves requires to get to phase 2. 146 | """ 147 | return max( 148 | self.tables.udslice_twist_prune[self.udslice[n], self.twist[n]], 149 | self.tables.udslice_flip_prune[self.udslice[n], self.flip[n]], 150 | ) 151 | 152 | def _phase_2_cost(self, n): 153 | """ 154 | Cost of current position for use in phase 2. Returns a lower bound on 155 | the number of moves required to get to a solved cube. 156 | """ 157 | return max( 158 | self.tables.edge4_corner_prune[self.edge4[n], self.corner[n]], 159 | self.tables.edge4_edge8_prune[self.edge4[n], self.edge8[n]], 160 | ) 161 | 162 | def _phase_1_search(self, n, depth): 163 | if time.time() > self._timeout: 164 | return -2 165 | elif self.min_dist_1[n] == 0: 166 | return self._phase_2_initialise(n) 167 | elif self.min_dist_1[n] <= depth: 168 | for i in range(6): 169 | if n > 0 and self.axis[n - 1] in (i, i + 3): 170 | # don't turn the same face on consecutive moves 171 | # also for opposite faces, e.g. U and D, UD = DU, so we can 172 | # impose that the lower index happens first. 173 | continue 174 | for j in range(1, 4): 175 | self.axis[n] = i 176 | self.power[n] = j 177 | mv = 3 * i + j - 1 178 | 179 | # update coordinates 180 | self.twist[n + 1] = self.tables.twist_move[self.twist[n]][ 181 | mv 182 | ] 183 | self.flip[n + 1] = self.tables.flip_move[self.flip[n]][mv] 184 | self.udslice[n + 1] = self.tables.udslice_move[ 185 | self.udslice[n] 186 | ][mv] 187 | self.min_dist_1[n + 1] = self._phase_1_cost(n + 1) 188 | 189 | # start search from next node 190 | m = self._phase_1_search(n + 1, depth - 1) 191 | if m >= 0: 192 | return m 193 | if m == -2: 194 | # time limit exceeded 195 | return -2 196 | # if no solution found at current depth, return -1 197 | return -1 198 | 199 | def _phase_2_search(self, n, depth): 200 | if self.min_dist_2[n] == 0: 201 | return n 202 | elif self.min_dist_2[n] <= depth: 203 | for i in range(6): 204 | if n > 0 and self.axis[n - 1] in (i, i + 3): 205 | continue 206 | for j in range(1, 4): 207 | if i in [1, 2, 4, 5] and j != 2: 208 | # in phase two we only allow half turns of the faces 209 | # R, F, L, B 210 | continue 211 | self.axis[n] = i 212 | self.power[n] = j 213 | mv = 3 * i + j - 1 214 | 215 | # update coordinates following the move mv 216 | self.edge4[n + 1] = self.tables.edge4_move[self.edge4[n]][ 217 | mv 218 | ] 219 | self.edge8[n + 1] = self.tables.edge8_move[self.edge8[n]][ 220 | mv 221 | ] 222 | self.corner[n + 1] = self.tables.corner_move[ 223 | self.corner[n] 224 | ][mv] 225 | self.min_dist_2[n + 1] = self._phase_2_cost(n + 1) 226 | 227 | # start search from new node 228 | m = self._phase_2_search(n + 1, depth - 1) 229 | if m >= 0: 230 | return m 231 | # if no moves lead to a tree with a solution or min_dist_2 > depth then 232 | # we return -1 to signify lack of solution 233 | return -1 234 | 235 | def _solution_to_string(self, length): 236 | """ 237 | Generate solution string. Uses standard cube notation: F means 238 | clockwise quarter turn of the F face, U' means a counter clockwise 239 | quarter turn of the U face, R2 means a half turn of the R face etc. 240 | """ 241 | 242 | def recover_move(axis_power): 243 | axis, power = axis_power 244 | if power == 1: 245 | return Color(axis).name 246 | if power == 2: 247 | return Color(axis).name + "2" 248 | if power == 3: 249 | return Color(axis).name + "'" 250 | raise RuntimeError("Invalid move in solution.") 251 | 252 | solution = map( 253 | recover_move, zip(self.axis[:length], self.power[:length]) 254 | ) 255 | return " ".join(solution) 256 | -------------------------------------------------------------------------------- /twophase/tables.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from .cubes.cubiecube import MOVE_CUBE, CubieCube 5 | 6 | 7 | class PruningTable: 8 | """ 9 | Helper class to allow pruning to be used as though they were 2-D tables 10 | """ 11 | 12 | def __init__(self, table, stride): 13 | self.table = table 14 | self.stride = stride 15 | 16 | def __getitem__(self, x): 17 | return self.table[x[0] * self.stride + x[1]] 18 | 19 | 20 | class Tables: 21 | """ 22 | Class for holding move and pruning tables in memory. 23 | 24 | Move tables are used for updating coordinate representation of cube when a 25 | particular move is applied. 26 | 27 | Pruning tables are used to obtain lower bounds for the number of moves 28 | required to reach a solution given a particular pair of coordinates. 29 | """ 30 | 31 | _tables_loaded = False 32 | 33 | # 3^7 possible corner orientations 34 | TWIST = 2187 35 | # 2^11 possible edge flips 36 | FLIP = 2048 37 | # 12C4 possible positions of FR, FL, BL, BR 38 | UDSLICE = 495 39 | # 4! possible permutations of FR, FL, BL, BR 40 | EDGE4 = 24 41 | # 8! possible permutations of UR, UF, UL, UB, DR, DF, DL, DB in phase two 42 | EDGE8 = 40320 43 | # 8! possible permutations of the corners 44 | CORNER = 40320 45 | # 12! possible permutations of all edges 46 | EDGE = 479001600 47 | # 6*3 possible moves 48 | MOVES = 18 49 | 50 | def __init__(self): 51 | if not self._tables_loaded: 52 | self.load_tables() 53 | 54 | @classmethod 55 | def load_tables(cls): 56 | if os.path.isfile("tables.json"): 57 | with open("tables.json", "r") as f: 58 | tables = json.load(f) 59 | cls.twist_move = tables["twist_move"] 60 | cls.flip_move = tables["flip_move"] 61 | cls.udslice_move = tables["udslice_move"] 62 | cls.edge4_move = tables["edge4_move"] 63 | cls.edge8_move = tables["edge8_move"] 64 | cls.corner_move = tables["corner_move"] 65 | cls.udslice_twist_prune = PruningTable( 66 | tables["udslice_twist_prune"], cls.TWIST 67 | ) 68 | cls.udslice_flip_prune = PruningTable( 69 | tables["udslice_flip_prune"], cls.FLIP 70 | ) 71 | cls.edge4_edge8_prune = PruningTable( 72 | tables["edge4_edge8_prune"], cls.EDGE8 73 | ) 74 | cls.edge4_corner_prune = PruningTable( 75 | tables["edge4_corner_prune"], cls.CORNER 76 | ) 77 | else: 78 | # ---------- Phase 1 move tables ---------- # 79 | cls.twist_move = cls.make_twist_table() 80 | cls.flip_move = cls.make_flip_table() 81 | cls.udslice_move = cls.make_udslice_table() 82 | 83 | # ---------- Phase 2 move tables ---------- # 84 | cls.edge4_move = cls.make_edge4_table() 85 | cls.edge8_move = cls.make_edge8_table() 86 | cls.corner_move = cls.make_corner_table() 87 | 88 | # ---------- Phase 1 pruning tables ---------- # 89 | cls.udslice_twist_prune = cls.make_udslice_twist_prune() 90 | cls.udslice_flip_prune = cls.make_udslice_flip_prune() 91 | 92 | # -------- Phase 2 pruning tables ---------- # 93 | cls.edge4_edge8_prune = cls.make_edge4_edge8_prune() 94 | cls.edge4_corner_prune = cls.make_edge4_corner_prune() 95 | 96 | tables = { 97 | "twist_move": cls.twist_move, 98 | "flip_move": cls.flip_move, 99 | "udslice_move": cls.udslice_move, 100 | "edge4_move": cls.edge4_move, 101 | "edge8_move": cls.edge8_move, 102 | "corner_move": cls.corner_move, 103 | "udslice_twist_prune": cls.udslice_twist_prune.table, 104 | "udslice_flip_prune": cls.udslice_flip_prune.table, 105 | "edge4_edge8_prune": cls.edge4_edge8_prune.table, 106 | "edge4_corner_prune": cls.edge4_corner_prune.table, 107 | } 108 | with open("tables.json", "w") as f: 109 | json.dump(tables, f) 110 | 111 | cls._tables_loaded = True 112 | 113 | @classmethod 114 | def make_twist_table(cls): 115 | twist_move = [[0] * cls.MOVES for i in range(cls.TWIST)] 116 | a = CubieCube() 117 | for i in range(cls.TWIST): 118 | a.twist = i 119 | for j in range(6): 120 | for k in range(3): 121 | a.corner_multiply(MOVE_CUBE[j]) 122 | twist_move[i][3 * j + k] = a.twist 123 | a.corner_multiply(MOVE_CUBE[j]) 124 | return twist_move 125 | 126 | @classmethod 127 | def make_flip_table(cls): 128 | flip_move = [[0] * cls.MOVES for i in range(cls.FLIP)] 129 | a = CubieCube() 130 | for i in range(cls.FLIP): 131 | a.flip = i 132 | for j in range(6): 133 | for k in range(3): 134 | a.edge_multiply(MOVE_CUBE[j]) 135 | flip_move[i][3 * j + k] = a.flip 136 | a.edge_multiply(MOVE_CUBE[j]) 137 | return flip_move 138 | 139 | @classmethod 140 | def make_udslice_table(cls): 141 | udslice_move = [[0] * cls.MOVES for i in range(cls.UDSLICE)] 142 | a = CubieCube() 143 | for i in range(cls.UDSLICE): 144 | a.udslice = i 145 | for j in range(6): 146 | for k in range(3): 147 | a.edge_multiply(MOVE_CUBE[j]) 148 | udslice_move[i][3 * j + k] = a.udslice 149 | a.edge_multiply(MOVE_CUBE[j]) 150 | return udslice_move 151 | 152 | @classmethod 153 | def make_edge4_table(cls): 154 | edge4_move = [[0] * cls.MOVES for i in range(cls.EDGE4)] 155 | a = CubieCube() 156 | for i in range(cls.EDGE4): 157 | a.edge4 = i 158 | for j in range(6): 159 | for k in range(3): 160 | a.edge_multiply(MOVE_CUBE[j]) 161 | if k % 2 == 0 and j % 3 != 0: 162 | edge4_move[i][3 * j + k] = -1 163 | else: 164 | edge4_move[i][3 * j + k] = a.edge4 165 | a.edge_multiply(MOVE_CUBE[j]) 166 | return edge4_move 167 | 168 | @classmethod 169 | def make_edge8_table(cls): 170 | edge8_move = [[0] * cls.MOVES for i in range(cls.EDGE8)] 171 | a = CubieCube() 172 | for i in range(cls.EDGE8): 173 | a.edge8 = i 174 | for j in range(6): 175 | for k in range(3): 176 | a.edge_multiply(MOVE_CUBE[j]) 177 | if k % 2 == 0 and j % 3 != 0: 178 | edge8_move[i][3 * j + k] = -1 179 | else: 180 | edge8_move[i][3 * j + k] = a.edge8 181 | a.edge_multiply(MOVE_CUBE[j]) 182 | return edge8_move 183 | 184 | @classmethod 185 | def make_corner_table(cls): 186 | corner_move = [[0] * cls.MOVES for i in range(cls.CORNER)] 187 | a = CubieCube() 188 | for i in range(cls.CORNER): 189 | a.corner = i 190 | for j in range(6): 191 | for k in range(3): 192 | a.corner_multiply(MOVE_CUBE[j]) 193 | if k % 2 == 0 and j % 3 != 0: 194 | corner_move[i][3 * j + k] = -1 195 | else: 196 | corner_move[i][3 * j + k] = a.corner 197 | a.corner_multiply(MOVE_CUBE[j]) 198 | return corner_move 199 | 200 | @classmethod 201 | def make_udslice_twist_prune(cls): 202 | udslice_twist_prune = [-1] * (cls.UDSLICE * cls.TWIST) 203 | udslice_twist_prune[0] = 0 204 | count, depth = 1, 0 205 | while count < cls.UDSLICE * cls.TWIST: 206 | for i in range(cls.UDSLICE * cls.TWIST): 207 | if udslice_twist_prune[i] == depth: 208 | m = [ 209 | cls.udslice_move[i // cls.TWIST][j] * cls.TWIST 210 | + cls.twist_move[i % cls.TWIST][j] 211 | for j in range(18) 212 | ] 213 | for x in m: 214 | if udslice_twist_prune[x] == -1: 215 | count += 1 216 | udslice_twist_prune[x] = depth + 1 217 | depth += 1 218 | return PruningTable(udslice_twist_prune, cls.TWIST) 219 | 220 | @classmethod 221 | def make_udslice_flip_prune(cls): 222 | udslice_flip_prune = [-1] * (cls.UDSLICE * cls.FLIP) 223 | udslice_flip_prune[0] = 0 224 | count, depth = 1, 0 225 | while count < cls.UDSLICE * cls.FLIP: 226 | for i in range(cls.UDSLICE * cls.FLIP): 227 | if udslice_flip_prune[i] == depth: 228 | m = [ 229 | cls.udslice_move[i // cls.FLIP][j] * cls.FLIP 230 | + cls.flip_move[i % cls.FLIP][j] 231 | for j in range(18) 232 | ] 233 | for x in m: 234 | if udslice_flip_prune[x] == -1: 235 | count += 1 236 | udslice_flip_prune[x] = depth + 1 237 | depth += 1 238 | return PruningTable(udslice_flip_prune, cls.FLIP) 239 | 240 | @classmethod 241 | def make_edge4_edge8_prune(cls): 242 | edge4_edge8_prune = [-1] * (cls.EDGE4 * cls.EDGE8) 243 | edge4_edge8_prune[0] = 0 244 | count, depth = 1, 0 245 | while count < cls.EDGE4 * cls.EDGE8: 246 | for i in range(cls.EDGE4 * cls.EDGE8): 247 | if edge4_edge8_prune[i] == depth: 248 | m = [ 249 | cls.edge4_move[i // cls.EDGE8][j] * cls.EDGE8 250 | + cls.edge8_move[i % cls.EDGE8][j] 251 | for j in range(18) 252 | ] 253 | for x in m: 254 | if edge4_edge8_prune[x] == -1: 255 | count += 1 256 | edge4_edge8_prune[x] = depth + 1 257 | depth += 1 258 | return PruningTable(edge4_edge8_prune, cls.EDGE8) 259 | 260 | @classmethod 261 | def make_edge4_corner_prune(cls): 262 | edge4_corner_prune = [-1] * (cls.EDGE4 * cls.CORNER) 263 | edge4_corner_prune[0] = 0 264 | count, depth = 1, 0 265 | while count < cls.EDGE4 * cls.CORNER: 266 | for i in range(cls.EDGE4 * cls.CORNER): 267 | if edge4_corner_prune[i] == depth: 268 | m = [ 269 | cls.edge4_move[i // cls.CORNER][j] * cls.CORNER 270 | + cls.corner_move[i % cls.CORNER][j] 271 | for j in range(18) 272 | ] 273 | for x in m: 274 | if edge4_corner_prune[x] == -1: 275 | count += 1 276 | edge4_corner_prune[x] = depth + 1 277 | depth += 1 278 | return PruningTable(edge4_corner_prune, cls.CORNER) 279 | --------------------------------------------------------------------------------