├── rubik ├── __init__.py ├── optimize.py ├── maths.py ├── cube.py └── solve.py ├── .gitignore ├── .bumpversion.cfg ├── .ruff.toml ├── tox.ini ├── .github └── workflows │ └── test.yaml ├── LICENSE ├── Makefile ├── setup.py ├── solve_random_cubes.py ├── README.md └── tests └── test.py /rubik/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .venv 3 | *.egg-info 4 | .tox 5 | build 6 | dist 7 | .python-version 8 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.2 3 | files = setup.py 4 | commit = True 5 | tag = False 6 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | 3 | ignore = [ 4 | # Multiple statements on one line. 5 | "E701", 6 | 7 | ] 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, py39, py310, py311 3 | skipsdist = true 4 | 5 | [testenv] 6 | usedevelop = true 7 | commands = python {toxinidir}/tests/test.py 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: "ubuntu-22.04" 9 | strategy: 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install ruff 23 | pip install -U -e . 24 | - name: Lint 25 | run: ruff --format=github . 26 | - name: Unit test 27 | run: python tests/test.py 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, 2020 Paul Glass 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | 3 | .PHONY: lint test test-all dist bump test-release release clean-dist clean 4 | 5 | VENV_EXE=python3 -m virtualenv 6 | VENV=.venv 7 | VENV_ACTIVATE=. $(VENV)/bin/activate 8 | BUMPTYPE=patch 9 | 10 | $(VENV): 11 | $(VENV_EXE) $(VENV) 12 | $(VENV_ACTIVATE); pip install -e . 13 | $(VENV_ACTIVATE); pip install build tox ruff bump2version twine wheel 'readme_renderer[md]' 14 | 15 | lint: $(VENV) 16 | $(VENV_ACTIVATE); ruff . 17 | 18 | test: $(VENV) 19 | $(VENV_ACTIVATE); python tests/test.py 20 | 21 | test-all: $(VENV) 22 | $(VENV_ACTIVATE); tox 23 | 24 | dist: clean-dist $(VENV) 25 | $(VENV_ACTIVATE); python -m build 26 | ls -ls dist 27 | tar tzf dist/*.tar.gz 28 | $(VENV_ACTIVATE); twine check dist/* 29 | 30 | bump: $(VENV) 31 | $(VENV_ACTIVATE); bump2version $(BUMPTYPE) 32 | git show -q 33 | @echo 34 | @echo "SUCCESS: Version was bumped and committed" 35 | 36 | test-release: clean test dist 37 | $(VENV_ACTIVATE); twine upload --repository-url https://test.pypi.org/legacy/ dist/* 38 | 39 | release: clean test dist 40 | $(VENV_ACTIVATE); twine upload dist/* 41 | 42 | clean-dist: 43 | rm -rf dist 44 | rm -rf *.egg-info 45 | 46 | clean: clean-dist 47 | rm -rf $(VENV) .tox 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | long_description = open("README.md").read() 4 | 5 | setup_params = dict( 6 | name="rubik-cube", 7 | version="0.0.2", 8 | license="MIT", 9 | author="Paul Glass", 10 | author_email="pnglass@gmail.com", 11 | url="https://github.com/pglass/cube", 12 | keywords="rubik cube solver", 13 | packages=["rubik"], 14 | package_data={"": ["LICENSE"]}, 15 | package_dir={"rubik": "rubik"}, 16 | include_package_data=True, 17 | description="A basic, pure-Python Rubik's cube solver", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | install_requires=[], 21 | classifiers=[ 22 | "Development Status :: 4 - Beta", 23 | "Environment :: Console", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | ], 33 | ) 34 | 35 | if __name__ == "__main__": 36 | setuptools.setup(**setup_params) 37 | -------------------------------------------------------------------------------- /solve_random_cubes.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from rubik import solve 4 | from rubik.cube import Cube 5 | from rubik.solve import Solver 6 | from rubik.optimize import optimize_moves 7 | 8 | SOLVED_CUBE_STR = "OOOOOOOOOYYYWWWGGGBBBYYYWWWGGGBBBYYYWWWGGGBBBRRRRRRRRR" 9 | MOVES = ["L", "R", "U", "D", "F", "B", "M", "E", "S"] 10 | 11 | 12 | def random_cube(): 13 | """ 14 | :return: A new scrambled Cube 15 | """ 16 | scramble_moves = " ".join(random.choices(MOVES, k=200)) 17 | a = Cube(SOLVED_CUBE_STR) 18 | a.sequence(scramble_moves) 19 | return a 20 | 21 | 22 | def run(): 23 | successes = 0 24 | failures = 0 25 | 26 | avg_opt_moves = 0.0 27 | avg_moves = 0.0 28 | avg_time = 0.0 29 | while True: 30 | C = random_cube() 31 | solver = Solver(C) 32 | 33 | start = time.time() 34 | solver.solve() 35 | duration = time.time() - start 36 | 37 | if C.is_solved(): 38 | opt_moves = optimize_moves(solver.moves) 39 | successes += 1 40 | avg_moves = (avg_moves * (successes - 1) + len(solver.moves)) / float(successes) 41 | avg_time = (avg_time * (successes - 1) + duration) / float(successes) 42 | avg_opt_moves = (avg_opt_moves * (successes - 1) + len(opt_moves)) / float(successes) 43 | else: 44 | failures += 1 45 | print(f"Failed ({successes + failures}): {C.flat_str()}") 46 | 47 | total = successes + failures 48 | if total == 1 or total % 100 == 0: 49 | pass_percentage = 100 * successes / total 50 | print(f"{total}: {successes} successes ({pass_percentage:0.3f}% passing)" 51 | f" avg_moves={avg_moves:0.3f} avg_opt_moves={avg_opt_moves:0.3f}" 52 | f" avg_time={avg_time:0.3f}s") 53 | 54 | 55 | if __name__ == '__main__': 56 | solve.DEBUG = False 57 | run() 58 | -------------------------------------------------------------------------------- /rubik/optimize.py: -------------------------------------------------------------------------------- 1 | from rubik import cube 2 | 3 | X_ROT_CW = { 4 | 'U': 'F', 5 | 'B': 'U', 6 | 'D': 'B', 7 | 'F': 'D', 8 | 'E': 'Si', 9 | 'S': 'E', 10 | 'Y': 'Z', 11 | 'Z': 'Yi', 12 | } 13 | Y_ROT_CW = { 14 | 'B': 'L', 15 | 'R': 'B', 16 | 'F': 'R', 17 | 'L': 'F', 18 | 'S': 'Mi', 19 | 'M': 'S', 20 | 'Z': 'X', 21 | 'X': 'Zi' 22 | } 23 | Z_ROT_CW = { 24 | 'U': 'L', 25 | 'R': 'U', 26 | 'D': 'R', 27 | 'L': 'D', 28 | 'E': 'Mi', 29 | 'M': 'E', 30 | 'Y': 'Xi', 31 | 'X': 'Y', 32 | } 33 | X_ROT_CC = {v: k for k, v in X_ROT_CW.items()} 34 | Y_ROT_CC = {v: k for k, v in Y_ROT_CW.items()} 35 | Z_ROT_CC = {v: k for k, v in Z_ROT_CW.items()} 36 | 37 | 38 | def get_rot_table(rot): 39 | if rot == 'X': return X_ROT_CW 40 | elif rot == 'Xi': return X_ROT_CC 41 | elif rot == 'Y': return Y_ROT_CW 42 | elif rot == 'Yi': return Y_ROT_CC 43 | elif rot == 'Z': return Z_ROT_CW 44 | elif rot == 'Zi': return Z_ROT_CC 45 | 46 | 47 | def _invert(move): 48 | if move.endswith('i'): 49 | return move[:1] 50 | return move + 'i' 51 | 52 | 53 | def apply_repeat_three_optimization(moves): 54 | """ R, R, R --> Ri """ 55 | changed = False 56 | i = 0 57 | while i < len(moves) - 2: 58 | if moves[i] == moves[i+1] == moves[i+2]: 59 | moves[i:i+3] = [_invert(moves[i])] 60 | changed = True 61 | else: 62 | i += 1 63 | if changed: 64 | apply_repeat_three_optimization(moves) 65 | 66 | 67 | def apply_do_undo_optimization(moves): 68 | """ R Ri --> , R R Ri Ri --> """ 69 | changed = False 70 | i = 0 71 | while i < len(moves) - 1: 72 | if _invert(moves[i]) == moves[i+1]: 73 | moves[i:i+2] = [] 74 | changed = True 75 | else: 76 | i += 1 77 | if changed: 78 | apply_do_undo_optimization(moves) 79 | 80 | 81 | def _unrotate(rot, moves): 82 | rot_table = get_rot_table(rot) 83 | result = [] 84 | for move in moves: 85 | if move in rot_table: 86 | result.append(rot_table[move]) 87 | elif _invert(move) in rot_table: 88 | result.append(_invert(rot_table[_invert(move)])) 89 | else: 90 | result.append(move) 91 | return result 92 | 93 | 94 | def apply_no_full_cube_rotation_optimization(moves): 95 | rots = {'X', 'Y', 'Z', 'Xi', 'Yi', 'Zi'} 96 | changed = False 97 | i = 0 98 | while i < len(moves): 99 | if moves[i] not in rots: 100 | i += 1 101 | continue 102 | 103 | for j in reversed(range(i + 1, len(moves))): 104 | if moves[j] == _invert(moves[i]): 105 | moves[i:j+1] = _unrotate(moves[i], moves[i+1:j]) 106 | changed = True 107 | break 108 | i += 1 109 | if changed: 110 | apply_no_full_cube_rotation_optimization(moves) 111 | 112 | 113 | def optimize_moves(moves): 114 | result = list(moves) 115 | apply_no_full_cube_rotation_optimization(result) 116 | apply_repeat_three_optimization(result) 117 | apply_do_undo_optimization(result) 118 | return result 119 | 120 | 121 | if __name__ == '__main__': 122 | test_seq_1 = ("Li Li E L Ei Li B Ei R E Ri Z E L Ei Li Zi U U Ui Ui Ui B U B B B Bi " 123 | "Ri B R Z U U Ui Ui Ui B U B B B Ri B B R Bi Bi D Bi Di Z Ri B B R Bi " 124 | "Bi D Bi Di Z B B Bi Ri B R Z B L Bi Li Bi Di B D B Bi Di B D B L Bi Li " 125 | "Z B B B Bi Di B D B L Bi Li Z B Bi Di B D B L Bi Li Z B B B L Bi Li Bi " 126 | "Di B D Z X X F F D F R Fi Ri Di Xi Xi X X Li Fi L D F Di Li F L F F Zi " 127 | "Li Fi L D F Di Li F L F F Z F Li Fi L D F Di Li F L F Li Fi L D F Di " 128 | "Li F L F F Xi Xi X X Ri Fi R Fi Ri F F R F F F R F Ri F R F F Ri F F F " 129 | "F Ri Fi R Fi Ri F F R F F F R F Ri F R F F Ri F F Xi Xi X X R R F D Ui " 130 | "R R Di U F R R R R F D Ui R R Di U F R R Z Z Z Z Z Z R R F D Ui R R Di " 131 | "U F R R Z Z Z Z R R F D Ui R R Di U F R R Z Z Z Z Z Ri S Ri Ri S S Ri " 132 | "Fi Fi R Si Si Ri Ri Si R Fi Fi Zi Xi Xi") 133 | moves = test_seq_1.split() 134 | print("{len(moves)} moves: {' '.join(moves)}") 135 | 136 | opt = optimize_moves(moves) 137 | print("{len(opt)} moves: {' '.join(opt)}") 138 | 139 | orig = cube.Cube("OOOOOOOOOYYYWWWGGGBBBYYYWWWGGGBBBYYYWWWGGGBBBRRRRRRRRR") 140 | c, d = cube.Cube(orig), cube.Cube(orig) 141 | 142 | c.sequence(" ".join(moves)) 143 | d.sequence(" ".join(opt)) 144 | print(c, '\n') 145 | print(d) 146 | assert c == d 147 | -------------------------------------------------------------------------------- /rubik/maths.py: -------------------------------------------------------------------------------- 1 | 2 | class Point: 3 | """A 3D point/vector""" 4 | 5 | def __init__(self, x, y=None, z=None): 6 | """Construct a Point from an (x, y, z) tuple or an iterable""" 7 | try: 8 | # convert from an iterable 9 | ii = iter(x) 10 | self.x = next(ii) 11 | self.y = next(ii) 12 | self.z = next(ii) 13 | except TypeError: 14 | # not iterable 15 | self.x = x 16 | self.y = y 17 | self.z = z 18 | if any(val is None for val in self): 19 | raise ValueError(f"Point does not allow None values: {self}") 20 | 21 | def __str__(self): 22 | return str(tuple(self)) 23 | 24 | def __repr__(self): 25 | return "Point" + str(self) 26 | 27 | def __add__(self, other): 28 | return Point(self.x + other.x, self.y + other.y, self.z + other.z) 29 | 30 | def __sub__(self, other): 31 | return Point(self.x - other.x, self.y - other.y, self.z - other.z) 32 | 33 | def __mul__(self, other): 34 | return Point(self.x * other, self.y * other, self.z * other) 35 | 36 | def dot(self, other): 37 | """Return the dot product""" 38 | return self.x * other.x + self.y * other.y + self.z * other.z 39 | 40 | def cross(self, other): 41 | return Point(self.y * other.z - self.z * other.y, 42 | self.z * other.x - self.x * other.z, 43 | self.x * other.y - self.y * other.x) 44 | 45 | def __getitem__(self, item): 46 | if item == 0: 47 | return self.x 48 | elif item == 1: 49 | return self.y 50 | elif item == 2: 51 | return self.z 52 | raise IndexError("Point index out of range") 53 | 54 | def __iter__(self): 55 | yield self.x 56 | yield self.y 57 | yield self.z 58 | 59 | def count(self, val): 60 | return int(self.x == val) + int(self.y == val) + int(self.z == val) 61 | 62 | def __iadd__(self, other): 63 | self.x += other.x 64 | self.y += other.y 65 | self.z += other.z 66 | return self 67 | 68 | def __isub__(self, other): 69 | self.x -= other.x 70 | self.y -= other.y 71 | self.z -= other.z 72 | return self 73 | 74 | def __eq__(self, other): 75 | if isinstance(other, (tuple, list)): 76 | return self.x == other[0] and self.y == other[1] and self.z == other[2] 77 | return (isinstance(other, self.__class__) and self.x == other.x 78 | and self.y == other.y and self.z == other.z) 79 | 80 | def __ne__(self, other): 81 | return not (self == other) 82 | 83 | 84 | class Matrix: 85 | """A 3x3 matrix""" 86 | 87 | def __init__(self, *args): 88 | """Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9) 89 | Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]) 90 | Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) 91 | Matrix(x for x in range(1, 10)) 92 | """ 93 | if len(args) == 9: 94 | self.vals = list(args) 95 | elif len(args) == 3: 96 | try: 97 | self.vals = [x for y in args for x in y] 98 | except Exception: 99 | self.vals = [] 100 | else: 101 | self.__init__(*args[0]) 102 | 103 | if len(self.vals) != 9: 104 | raise ValueError(f"Matrix requires 9 items, got {args}") 105 | 106 | def __str__(self): 107 | return ("[{}, {}, {},\n" 108 | " {}, {}, {},\n" 109 | " {}, {}, {}]".format(*self.vals)) 110 | 111 | def __repr__(self): 112 | return ("Matrix({}, {}, {},\n" 113 | " {}, {}, {},\n" 114 | " {}, {}, {})".format(*self.vals)) 115 | 116 | def __eq__(self, other): 117 | return self.vals == other.vals 118 | 119 | def __add__(self, other): 120 | return Matrix(a + b for a, b in zip(self.vals, other.vals)) 121 | 122 | def __sub__(self, other): 123 | return Matrix(a - b for a, b in zip(self.vals, other.vals)) 124 | 125 | def __iadd__(self, other): 126 | self.vals = [a + b for a, b in zip(self.vals, other.vals)] 127 | return self 128 | 129 | def __isub__(self, other): 130 | self.vals = [a - b for a, b in zip(self.vals, other.vals)] 131 | return self 132 | 133 | def __mul__(self, other): 134 | """Do Matrix-Matrix or Matrix-Point multiplication.""" 135 | if isinstance(other, Point): 136 | return Point(other.dot(Point(row)) for row in self.rows()) 137 | elif isinstance(other, Matrix): 138 | return Matrix(Point(row).dot(Point(col)) for row in self.rows() for col in other.cols()) 139 | 140 | def rows(self): 141 | yield self.vals[0:3] 142 | yield self.vals[3:6] 143 | yield self.vals[6:9] 144 | 145 | def cols(self): 146 | yield self.vals[0:9:3] 147 | yield self.vals[1:9:3] 148 | yield self.vals[2:9:3] 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI](https://img.shields.io/pypi/v/rubik-cube) 2 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rubik-cube) 3 | 4 | # Overview 5 | 6 | This is a Python 3 implementation of a (3x3) Rubik's Cube solver. 7 | 8 | It contains: 9 | 10 | - A simple implementation of the cube 11 | - A solver that follows a fixed algorithm 12 | - An unintelligent solution sequence optimizer 13 | - A decent set of test cases 14 | 15 | ## Installation 16 | 17 | The package is hosted on PyPI. 18 | 19 | ``` 20 | pip install rubik-cube 21 | ``` 22 | 23 | Import from the `rubik` package, 24 | 25 | ```python 26 | >>> from rubik.cube import Cube 27 | >>> c = Cube("OOOOOOOOOYYYWWWGGGBBBYYYWWWGGGBBBYYYWWWGGGBBBRRRRRRRRR") 28 | >>> print(c) 29 | OOO 30 | OOO 31 | OOO 32 | YYY WWW GGG BBB 33 | YYY WWW GGG BBB 34 | YYY WWW GGG BBB 35 | RRR 36 | RRR 37 | RRR 38 | ``` 39 | 40 | ## Implementation 41 | 42 | ### Piece 43 | 44 | The cornerstone of this implementation is the Piece class. A Piece stores two 45 | pieces of information: 46 | 47 | 1. An integer `position` vector `(x, y, z)` where each component is in {-1, 0, 48 | 1}: 49 | - `(0, 0, 0)` is the center of the cube 50 | - the positive x-axis points to the right face 51 | - the positive y-axis points to the up face 52 | - the positive z-axis points to the front face 53 | 54 | 2. A `colors` vector `(cx, cy, cz)`, giving the color of the sticker along each 55 | axis. Null values are place whenever that Piece has less than three sides. For 56 | example, a Piece with `colors=('Orange', None, 'Red')` is an edge piece with an 57 | `'Orange'` sticker facing the x-direction and a `'Red'` sticker facing the 58 | z-direction. The Piece doesn't know or care which direction along the x-axis 59 | the `'Orange'` sticker is facing, just that it is facing in the x-direction and 60 | not the y- or z- directions. 61 | 62 | Using the combination of `position` and `color` vectors makes it easy to 63 | identify any Piece by its absolute position or by its unique combination of 64 | colors. 65 | 66 | A Piece provides a method `Piece.rotate(matrix)`, which accepts a (90 degree) 67 | rotation matrix. A matrix-vector multiplication is done to update the Piece's 68 | `position` vector. Then we update the `colors` vector, by swapping exactly two 69 | entries in the `colors` vector: 70 | 71 | - For example, a corner Piece has three stickers of different colors. After a 72 | 90 degree rotation of the Piece, one sticker remains facing down the same 73 | axis, while the other two stickers swap axes. This corresponds to swapping the 74 | positions of two entries in the Piece’s `colors` vector. 75 | - For an edge or face piece, the argument is the same as above, although we may 76 | swap around one or more null entries. 77 | 78 | ### Cube 79 | 80 | The Cube class is built on top of the Piece class. The Cube stores a list of 81 | Pieces and provides nice methods for flipping slices of the cube, as well as 82 | methods for querying the current state. (I followed standard [Rubik's Cube 83 | notation](http://ruwix.com/the-rubiks-cube/notation/)) 84 | 85 | Because the Piece class encapsulates all of the rotation logic, implementing 86 | rotations in the Cube class is dead simple - just apply the appropriate 87 | rotation matrix to all Pieces involved in the rotation. An example: To 88 | implement `Cube.L()` - a clockwise rotation of the left face - do the 89 | following: 90 | 91 | 1. Construct the appropriate [rotation matrix]( 92 | http://en.wikipedia.org/wiki/Rotation_matrix) for a 90 degree rotation in the 93 | `x = -1` plane. 94 | 2. Select all Pieces satisfying `position.x == -1`. 95 | 3. Apply the rotation matrix to each of these Pieces. 96 | 97 | To implement `Cube.X()` - a clockwise rotation of the entire cube around the 98 | positive x-axis - just apply a rotation matrix to all Pieces stored in the 99 | Cube. 100 | 101 | ### Solver 102 | 103 | The solver implements the algorithm described 104 | [here](http://www.chessandpoker.com/rubiks-cube-solution.html). It is a 105 | layer-by-layer solution. First the front-face (the `z = 1` plane) is solved, 106 | then the middle layer (`z = 0`), and finally the back layer (`z = -1`). When 107 | the solver is done, `Solver.moves` is a list representing the solution 108 | sequence. 109 | 110 | My first correct-looking implementation of the solver average 252.5 moves per 111 | solution sequence on 135000 randomly-generated cubes (with no failures). 112 | Implementing a dumb optimizer reduced the average number of moves to 192.7 on 113 | 67000 randomly-generated cubes. The optimizer does the following: 114 | 115 | 1. Eliminate full-cube rotations by "unrotating" the moves (Z U L D Zi becomes 116 | L D R) 117 | 2. Eliminate moves followed by their inverse (R R Ri Ri is gone) 118 | 3. Replace moves repeated three times with a single turn in the opposite 119 | direction (R R R becomes Ri) 120 | 121 | The solver is not particularly fast. On my machine (a 4.0 Ghz i7), it takes 122 | about 0.06 seconds per solve on CPython, which is roughly 16.7 solves/second. 123 | On PyPy, this is reduced to about 0.013 seconds per solve, or about 76 124 | solves/second. 125 | -------------------------------------------------------------------------------- /rubik/cube.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from rubik.maths import Point, Matrix 4 | 5 | RIGHT = X_AXIS = Point(1, 0, 0) 6 | LEFT = Point(-1, 0, 0) 7 | UP = Y_AXIS = Point(0, 1, 0) 8 | DOWN = Point(0, -1, 0) 9 | FRONT = Z_AXIS = Point(0, 0, 1) 10 | BACK = Point(0, 0, -1) 11 | 12 | FACE = 'face' 13 | EDGE = 'edge' 14 | CORNER = 'corner' 15 | 16 | 17 | # 90 degree rotations in the XY plane. CW is clockwise, CC is counter-clockwise. 18 | ROT_XY_CW = Matrix(0, 1, 0, 19 | -1, 0, 0, 20 | 0, 0, 1) 21 | ROT_XY_CC = Matrix(0, -1, 0, 22 | 1, 0, 0, 23 | 0, 0, 1) 24 | 25 | # 90 degree rotations in the XZ plane (around the y-axis when viewed pointing toward you). 26 | ROT_XZ_CW = Matrix(0, 0, -1, 27 | 0, 1, 0, 28 | 1, 0, 0) 29 | ROT_XZ_CC = Matrix(0, 0, 1, 30 | 0, 1, 0, 31 | -1, 0, 0) 32 | 33 | # 90 degree rotations in the YZ plane (around the x-axis when viewed pointing toward you). 34 | ROT_YZ_CW = Matrix(1, 0, 0, 35 | 0, 0, 1, 36 | 0, -1, 0) 37 | ROT_YZ_CC = Matrix(1, 0, 0, 38 | 0, 0, -1, 39 | 0, 1, 0) 40 | 41 | 42 | def get_rot_from_face(face): 43 | """ 44 | :param face: One of FRONT, BACK, LEFT, RIGHT, UP, DOWN 45 | :return: A pair (CW, CC) given the clockwise and counterclockwise rotations for that face 46 | """ 47 | if face == RIGHT: return "R", "Ri" 48 | elif face == LEFT: return "L", "Li" 49 | elif face == UP: return "U", "Ui" 50 | elif face == DOWN: return "D", "Di" 51 | elif face == FRONT: return "F", "Fi" 52 | elif face == BACK: return "B", "Bi" 53 | return None 54 | 55 | 56 | class Piece: 57 | 58 | def __init__(self, pos, colors): 59 | """ 60 | :param pos: A tuple of integers (x, y, z) each ranging from -1 to 1 61 | :param colors: A tuple of length three (x, y, z) where each component gives the color 62 | of the side of the piece on that axis (if it exists), or None. 63 | """ 64 | assert all(type(x) == int and x in (-1, 0, 1) for x in pos) 65 | assert len(colors) == 3 66 | self.pos = pos 67 | self.colors = list(colors) 68 | self._set_piece_type() 69 | 70 | def __str__(self): 71 | colors = "".join(c for c in self.colors if c is not None) 72 | return f"({self.type}, {colors}, {self.pos})" 73 | 74 | def _set_piece_type(self): 75 | if self.colors.count(None) == 2: 76 | self.type = FACE 77 | elif self.colors.count(None) == 1: 78 | self.type = EDGE 79 | elif self.colors.count(None) == 0: 80 | self.type = CORNER 81 | else: 82 | raise ValueError(f"Must have 1, 2, or 3 colors - given colors={self.colors}") 83 | 84 | def rotate(self, matrix): 85 | """Apply the given rotation matrix to this piece.""" 86 | before = self.pos 87 | self.pos = matrix * self.pos 88 | 89 | # we need to swap the positions of two things in self.colors so colors appear 90 | # on the correct faces. rot gives us the axes to swap between. 91 | rot = self.pos - before 92 | if not any(rot): 93 | return # no change occurred 94 | if rot.count(0) == 2: 95 | rot += matrix * rot 96 | 97 | assert rot.count(0) == 1, ( 98 | f"There is a bug in the Piece.rotate() method!" 99 | f"\nbefore: {before}" 100 | f"\nself.pos: {self.pos}" 101 | f"\nrot: {rot}" 102 | ) 103 | 104 | i, j = (i for i, x in enumerate(rot) if x != 0) 105 | self.colors[i], self.colors[j] = self.colors[j], self.colors[i] 106 | 107 | 108 | class Cube: 109 | """Stores Pieces which are addressed through an x-y-z coordinate system: 110 | -x is the LEFT direction, +x is the RIGHT direction 111 | -y is the DOWN direction, +y is the UP direction 112 | -z is the BACK direction, +z is the FRONT direction 113 | """ 114 | 115 | def _from_cube(self, c): 116 | self.faces = [Piece(pos=Point(p.pos), colors=p.colors) for p in c.faces] 117 | self.edges = [Piece(pos=Point(p.pos), colors=p.colors) for p in c.edges] 118 | self.corners = [Piece(pos=Point(p.pos), colors=p.colors) for p in c.corners] 119 | self.pieces = self.faces + self.edges + self.corners 120 | 121 | def _assert_data(self): 122 | assert len(self.pieces) == 26 123 | assert all(p.type == FACE for p in self.faces) 124 | assert all(p.type == EDGE for p in self.edges) 125 | assert all(p.type == CORNER for p in self.corners) 126 | 127 | def __init__(self, cube_str): 128 | """ 129 | cube_str looks like: 130 | UUU 0 1 2 131 | UUU 3 4 5 132 | UUU 6 7 8 133 | LLL FFF RRR BBB 9 10 11 12 13 14 15 16 17 18 19 20 134 | LLL FFF RRR BBB 21 22 23 24 25 26 27 28 29 30 31 32 135 | LLL FFF RRR BBB 33 34 35 36 37 38 39 40 41 42 43 44 136 | DDD 45 46 47 137 | DDD 48 49 50 138 | DDD 51 52 53 139 | Note that the back side is mirrored in the horizontal axis during unfolding. 140 | Each 'sticker' must be a single character. 141 | """ 142 | if isinstance(cube_str, Cube): 143 | self._from_cube(cube_str) 144 | return 145 | 146 | cube_str = "".join(x for x in cube_str if x not in string.whitespace) 147 | assert len(cube_str) == 54 148 | self.faces = ( 149 | Piece(pos=RIGHT, colors=(cube_str[28], None, None)), 150 | Piece(pos=LEFT, colors=(cube_str[22], None, None)), 151 | Piece(pos=UP, colors=(None, cube_str[4], None)), 152 | Piece(pos=DOWN, colors=(None, cube_str[49], None)), 153 | Piece(pos=FRONT, colors=(None, None, cube_str[25])), 154 | Piece(pos=BACK, colors=(None, None, cube_str[31]))) 155 | self.edges = ( 156 | Piece(pos=RIGHT + UP, colors=(cube_str[16], cube_str[5], None)), 157 | Piece(pos=RIGHT + DOWN, colors=(cube_str[40], cube_str[50], None)), 158 | Piece(pos=RIGHT + FRONT, colors=(cube_str[27], None, cube_str[26])), 159 | Piece(pos=RIGHT + BACK, colors=(cube_str[29], None, cube_str[30])), 160 | Piece(pos=LEFT + UP, colors=(cube_str[10], cube_str[3], None)), 161 | Piece(pos=LEFT + DOWN, colors=(cube_str[34], cube_str[48], None)), 162 | Piece(pos=LEFT + FRONT, colors=(cube_str[23], None, cube_str[24])), 163 | Piece(pos=LEFT + BACK, colors=(cube_str[21], None, cube_str[32])), 164 | Piece(pos=UP + FRONT, colors=(None, cube_str[7], cube_str[13])), 165 | Piece(pos=UP + BACK, colors=(None, cube_str[1], cube_str[19])), 166 | Piece(pos=DOWN + FRONT, colors=(None, cube_str[46], cube_str[37])), 167 | Piece(pos=DOWN + BACK, colors=(None, cube_str[52], cube_str[43])), 168 | ) 169 | self.corners = ( 170 | Piece(pos=RIGHT + UP + FRONT, colors=(cube_str[15], cube_str[8], cube_str[14])), 171 | Piece(pos=RIGHT + UP + BACK, colors=(cube_str[17], cube_str[2], cube_str[18])), 172 | Piece(pos=RIGHT + DOWN + FRONT, colors=(cube_str[39], cube_str[47], cube_str[38])), 173 | Piece(pos=RIGHT + DOWN + BACK, colors=(cube_str[41], cube_str[53], cube_str[42])), 174 | Piece(pos=LEFT + UP + FRONT, colors=(cube_str[11], cube_str[6], cube_str[12])), 175 | Piece(pos=LEFT + UP + BACK, colors=(cube_str[9], cube_str[0], cube_str[20])), 176 | Piece(pos=LEFT + DOWN + FRONT, colors=(cube_str[35], cube_str[45], cube_str[36])), 177 | Piece(pos=LEFT + DOWN + BACK, colors=(cube_str[33], cube_str[51], cube_str[44])), 178 | ) 179 | 180 | self.pieces = self.faces + self.edges + self.corners 181 | 182 | self._assert_data() 183 | 184 | def is_solved(self): 185 | def check(colors): 186 | assert len(colors) == 9 187 | return all(c == colors[0] for c in colors) 188 | return (check([piece.colors[2] for piece in self._face(FRONT)]) and 189 | check([piece.colors[2] for piece in self._face(BACK)]) and 190 | check([piece.colors[1] for piece in self._face(UP)]) and 191 | check([piece.colors[1] for piece in self._face(DOWN)]) and 192 | check([piece.colors[0] for piece in self._face(LEFT)]) and 193 | check([piece.colors[0] for piece in self._face(RIGHT)])) 194 | 195 | def _face(self, axis): 196 | """ 197 | :param axis: One of LEFT, RIGHT, UP, DOWN, FRONT, BACK 198 | :return: A list of Pieces on the given face 199 | """ 200 | assert axis.count(0) == 2 201 | return [p for p in self.pieces if p.pos.dot(axis) > 0] 202 | 203 | def _slice(self, plane): 204 | """ 205 | :param plane: A sum of any two of X_AXIS, Y_AXIS, Z_AXIS (e.g. X_AXIS + Y_AXIS) 206 | :return: A list of Pieces in the given plane 207 | """ 208 | assert plane.count(0) == 1 209 | i = next((i for i, x in enumerate(plane) if x == 0)) 210 | return [p for p in self.pieces if p.pos[i] == 0] 211 | 212 | def _rotate_face(self, face, matrix): 213 | self._rotate_pieces(self._face(face), matrix) 214 | 215 | def _rotate_slice(self, plane, matrix): 216 | self._rotate_pieces(self._slice(plane), matrix) 217 | 218 | def _rotate_pieces(self, pieces, matrix): 219 | for piece in pieces: 220 | piece.rotate(matrix) 221 | 222 | # Rubik's Cube Notation: http://ruwix.com/the-rubiks-cube/notation/ 223 | def L(self): self._rotate_face(LEFT, ROT_YZ_CC) 224 | def Li(self): self._rotate_face(LEFT, ROT_YZ_CW) 225 | def R(self): self._rotate_face(RIGHT, ROT_YZ_CW) 226 | def Ri(self): self._rotate_face(RIGHT, ROT_YZ_CC) 227 | def U(self): self._rotate_face(UP, ROT_XZ_CW) 228 | def Ui(self): self._rotate_face(UP, ROT_XZ_CC) 229 | def D(self): self._rotate_face(DOWN, ROT_XZ_CC) 230 | def Di(self): self._rotate_face(DOWN, ROT_XZ_CW) 231 | def F(self): self._rotate_face(FRONT, ROT_XY_CW) 232 | def Fi(self): self._rotate_face(FRONT, ROT_XY_CC) 233 | def B(self): self._rotate_face(BACK, ROT_XY_CC) 234 | def Bi(self): self._rotate_face(BACK, ROT_XY_CW) 235 | def M(self): self._rotate_slice(Y_AXIS + Z_AXIS, ROT_YZ_CC) 236 | def Mi(self): self._rotate_slice(Y_AXIS + Z_AXIS, ROT_YZ_CW) 237 | def E(self): self._rotate_slice(X_AXIS + Z_AXIS, ROT_XZ_CC) 238 | def Ei(self): self._rotate_slice(X_AXIS + Z_AXIS, ROT_XZ_CW) 239 | def S(self): self._rotate_slice(X_AXIS + Y_AXIS, ROT_XY_CW) 240 | def Si(self): self._rotate_slice(X_AXIS + Y_AXIS, ROT_XY_CC) 241 | def X(self): self._rotate_pieces(self.pieces, ROT_YZ_CW) 242 | def Xi(self): self._rotate_pieces(self.pieces, ROT_YZ_CC) 243 | def Y(self): self._rotate_pieces(self.pieces, ROT_XZ_CW) 244 | def Yi(self): self._rotate_pieces(self.pieces, ROT_XZ_CC) 245 | def Z(self): self._rotate_pieces(self.pieces, ROT_XY_CW) 246 | def Zi(self): self._rotate_pieces(self.pieces, ROT_XY_CC) 247 | 248 | def sequence(self, move_str): 249 | """ 250 | :param moves: A string containing notated moves separated by spaces: "L Ri U M Ui B M" 251 | """ 252 | moves = [getattr(self, name) for name in move_str.split()] 253 | for move in moves: 254 | move() 255 | 256 | def find_piece(self, *colors): 257 | if None in colors: 258 | return 259 | for p in self.pieces: 260 | if p.colors.count(None) == 3 - len(colors) \ 261 | and all(c in p.colors for c in colors): 262 | return p 263 | 264 | def get_piece(self, x, y, z): 265 | """ 266 | :return: the Piece at the given Point 267 | """ 268 | point = Point(x, y, z) 269 | for p in self.pieces: 270 | if p.pos == point: 271 | return p 272 | 273 | def __getitem__(self, *args): 274 | if len(args) == 1: 275 | return self.get_piece(*args[0]) 276 | return self.get_piece(*args) 277 | 278 | def __eq__(self, other): 279 | return isinstance(other, Cube) and self._color_list() == other._color_list() 280 | 281 | def __ne__(self, other): 282 | return not (self == other) 283 | 284 | def colors(self): 285 | """ 286 | :return: A set containing the colors of all stickers on the cube 287 | """ 288 | return set(c for piece in self.pieces for c in piece.colors if c is not None) 289 | 290 | def left_color(self): return self[LEFT].colors[0] 291 | def right_color(self): return self[RIGHT].colors[0] 292 | def up_color(self): return self[UP].colors[1] 293 | def down_color(self): return self[DOWN].colors[1] 294 | def front_color(self): return self[FRONT].colors[2] 295 | def back_color(self): return self[BACK].colors[2] 296 | 297 | def _color_list(self): 298 | right = [p.colors[0] for p in sorted(self._face(RIGHT), key=lambda p: (-p.pos.y, -p.pos.z))] 299 | left = [p.colors[0] for p in sorted(self._face(LEFT), key=lambda p: (-p.pos.y, p.pos.z))] 300 | up = [p.colors[1] for p in sorted(self._face(UP), key=lambda p: (p.pos.z, p.pos.x))] 301 | down = [p.colors[1] for p in sorted(self._face(DOWN), key=lambda p: (-p.pos.z, p.pos.x))] 302 | front = [p.colors[2] for p in sorted(self._face(FRONT), key=lambda p: (-p.pos.y, p.pos.x))] 303 | back = [p.colors[2] for p in sorted(self._face(BACK), key=lambda p: (-p.pos.y, -p.pos.x))] 304 | 305 | return (up + left[0:3] + front[0:3] + right[0:3] + back[0:3] 306 | + left[3:6] + front[3:6] + right[3:6] + back[3:6] 307 | + left[6:9] + front[6:9] + right[6:9] + back[6:9] + down) 308 | 309 | def flat_str(self): 310 | return "".join(x for x in str(self) if x not in string.whitespace) 311 | 312 | def __str__(self): 313 | template = (" {}{}{}\n" 314 | " {}{}{}\n" 315 | " {}{}{}\n" 316 | "{}{}{} {}{}{} {}{}{} {}{}{}\n" 317 | "{}{}{} {}{}{} {}{}{} {}{}{}\n" 318 | "{}{}{} {}{}{} {}{}{} {}{}{}\n" 319 | " {}{}{}\n" 320 | " {}{}{}\n" 321 | " {}{}{}") 322 | 323 | return " " + template.format(*self._color_list()).strip() 324 | 325 | 326 | if __name__ == '__main__': 327 | cube = Cube(" DLU\n" 328 | " RRD\n" 329 | " FFU\n" 330 | "BBL DDR BRB LDL\n" 331 | "RBF RUU LFB DDU\n" 332 | "FBR BBR FUD FLU\n" 333 | " DLU\n" 334 | " ULF\n" 335 | " LFR") 336 | print(cube) 337 | -------------------------------------------------------------------------------- /rubik/solve.py: -------------------------------------------------------------------------------- 1 | from rubik import cube 2 | from rubik.maths import Point 3 | 4 | DEBUG = False 5 | 6 | 7 | class Solver: 8 | 9 | def __init__(self, c): 10 | self.cube = c 11 | self.colors = c.colors() 12 | self.moves = [] 13 | 14 | self.left_piece = self.cube.find_piece(self.cube.left_color()) 15 | self.right_piece = self.cube.find_piece(self.cube.right_color()) 16 | self.up_piece = self.cube.find_piece(self.cube.up_color()) 17 | self.down_piece = self.cube.find_piece(self.cube.down_color()) 18 | 19 | self.inifinite_loop_max_iterations = 12 20 | 21 | def solve(self): 22 | if DEBUG: print(self.cube) 23 | self.cross() 24 | if DEBUG: print('Cross:\n', self.cube) 25 | self.cross_corners() 26 | if DEBUG: print('Corners:\n', self.cube) 27 | self.second_layer() 28 | if DEBUG: print('Second layer:\n', self.cube) 29 | self.back_face_edges() 30 | if DEBUG: print('Last layer edges\n', self.cube) 31 | self.last_layer_corners_position() 32 | if DEBUG: print('Last layer corners -- position\n', self.cube) 33 | self.last_layer_corners_orientation() 34 | if DEBUG: print('Last layer corners -- orientation\n', self.cube) 35 | self.last_layer_edges() 36 | if DEBUG: print('Solved\n', self.cube) 37 | 38 | def move(self, move_str): 39 | self.moves.extend(move_str.split()) 40 | self.cube.sequence(move_str) 41 | 42 | def cross(self): 43 | if DEBUG: print("cross") 44 | # place the UP-LEFT piece 45 | fl_piece = self.cube.find_piece(self.cube.front_color(), self.cube.left_color()) 46 | fr_piece = self.cube.find_piece(self.cube.front_color(), self.cube.right_color()) 47 | fu_piece = self.cube.find_piece(self.cube.front_color(), self.cube.up_color()) 48 | fd_piece = self.cube.find_piece(self.cube.front_color(), self.cube.down_color()) 49 | 50 | self._cross_left_or_right(fl_piece, self.left_piece, self.cube.left_color(), "L L", "E L Ei Li") 51 | self._cross_left_or_right(fr_piece, self.right_piece, self.cube.right_color(), "R R", "Ei R E Ri") 52 | 53 | self.move("Z") 54 | self._cross_left_or_right(fd_piece, self.down_piece, self.cube.left_color(), "L L", "E L Ei Li") 55 | self._cross_left_or_right(fu_piece, self.up_piece, self.cube.right_color(), "R R", "Ei R E Ri") 56 | self.move("Zi") 57 | 58 | def _cross_left_or_right(self, edge_piece, face_piece, face_color, move_1, move_2): 59 | # don't do anything if piece is in correct place 60 | if (edge_piece.pos == (face_piece.pos.x, face_piece.pos.y, 1) 61 | and edge_piece.colors[2] == self.cube.front_color()): 62 | return 63 | 64 | # ensure piece is at z = -1 65 | undo_move = None 66 | if edge_piece.pos.z == 0: 67 | pos = Point(edge_piece.pos) 68 | pos.x = 0 # pick the UP or DOWN face 69 | cw, cc = cube.get_rot_from_face(pos) 70 | 71 | if edge_piece.pos in (cube.LEFT + cube.UP, cube.RIGHT + cube.DOWN): 72 | self.move(cw) 73 | undo_move = cc 74 | else: 75 | self.move(cc) 76 | undo_move = cw 77 | elif edge_piece.pos.z == 1: 78 | pos = Point(edge_piece.pos) 79 | pos.z = 0 80 | cw, cc = cube.get_rot_from_face(pos) 81 | self.move("{0} {0}".format(cc)) 82 | # don't set the undo move if the piece starts out in the right position 83 | # (with wrong orientation) or we'll screw up the remainder of the algorithm 84 | if edge_piece.pos.x != face_piece.pos.x: 85 | undo_move = "{0} {0}".format(cw) 86 | 87 | assert edge_piece.pos.z == -1 88 | 89 | # piece is at z = -1, rotate to correct face (LEFT or RIGHT) 90 | count = 0 91 | while (edge_piece.pos.x, edge_piece.pos.y) != (face_piece.pos.x, face_piece.pos.y): 92 | self.move("B") 93 | count += 1 94 | if count >= self.inifinite_loop_max_iterations: 95 | raise Exception("Stuck in loop - unsolvable cube?\n" + str(self.cube)) 96 | 97 | # if we moved a correctly-placed piece, restore it 98 | if undo_move: 99 | self.move(undo_move) 100 | 101 | # the piece is on the correct face on plane z = -1, but has two orientations 102 | if edge_piece.colors[0] == face_color: 103 | self.move(move_1) 104 | else: 105 | self.move(move_2) 106 | 107 | def cross_corners(self): 108 | if DEBUG: print("cross_corners") 109 | fld_piece = self.cube.find_piece(self.cube.front_color(), self.cube.left_color(), self.cube.down_color()) 110 | flu_piece = self.cube.find_piece(self.cube.front_color(), self.cube.left_color(), self.cube.up_color()) 111 | frd_piece = self.cube.find_piece(self.cube.front_color(), self.cube.right_color(), self.cube.down_color()) 112 | fru_piece = self.cube.find_piece(self.cube.front_color(), self.cube.right_color(), self.cube.up_color()) 113 | 114 | self.place_frd_corner(frd_piece, self.right_piece, self.down_piece, self.cube.front_color()) 115 | self.move("Z") 116 | self.place_frd_corner(fru_piece, self.up_piece, self.right_piece, self.cube.front_color()) 117 | self.move("Z") 118 | self.place_frd_corner(flu_piece, self.left_piece, self.up_piece, self.cube.front_color()) 119 | self.move("Z") 120 | self.place_frd_corner(fld_piece, self.down_piece, self.left_piece, self.cube.front_color()) 121 | self.move("Z") 122 | 123 | def place_frd_corner(self, corner_piece, right_piece, down_piece, front_color): 124 | # rotate to z = -1 125 | if corner_piece.pos.z == 1: 126 | pos = Point(corner_piece.pos) 127 | pos.x = pos.z = 0 128 | cw, cc = cube.get_rot_from_face(pos) 129 | 130 | # be careful not to screw up other pieces on the front face 131 | count = 0 132 | undo_move = cc 133 | while corner_piece.pos.z != -1: 134 | self.move(cw) 135 | count += 1 136 | 137 | if count > 1: 138 | # go the other direction because I don't know which is which. 139 | # we need to do only one flip (net) or we'll move other 140 | # correctly-placed corners out of place. 141 | for _ in range(count): 142 | self.move(cc) 143 | 144 | count = 0 145 | while corner_piece.pos.z != -1: 146 | self.move(cc) 147 | count += 1 148 | undo_move = cw 149 | self.move("B") 150 | for _ in range(count): 151 | self.move(undo_move) 152 | 153 | # rotate piece to be directly below its destination 154 | while (corner_piece.pos.x, corner_piece.pos.y) != (right_piece.pos.x, down_piece.pos.y): 155 | self.move("B") 156 | 157 | # there are three possible orientations for a corner 158 | if corner_piece.colors[0] == front_color: 159 | self.move("B D Bi Di") 160 | elif corner_piece.colors[1] == front_color: 161 | self.move("Bi Ri B R") 162 | else: 163 | self.move("Ri B B R Bi Bi D Bi Di") 164 | 165 | def second_layer(self): 166 | rd_piece = self.cube.find_piece(self.cube.right_color(), self.cube.down_color()) 167 | ru_piece = self.cube.find_piece(self.cube.right_color(), self.cube.up_color()) 168 | ld_piece = self.cube.find_piece(self.cube.left_color(), self.cube.down_color()) 169 | lu_piece = self.cube.find_piece(self.cube.left_color(), self.cube.up_color()) 170 | 171 | self.place_middle_layer_ld_edge(ld_piece, self.cube.left_color(), self.cube.down_color()) 172 | self.move("Z") 173 | self.place_middle_layer_ld_edge(rd_piece, self.cube.left_color(), self.cube.down_color()) 174 | self.move("Z") 175 | self.place_middle_layer_ld_edge(ru_piece, self.cube.left_color(), self.cube.down_color()) 176 | self.move("Z") 177 | self.place_middle_layer_ld_edge(lu_piece, self.cube.left_color(), self.cube.down_color()) 178 | self.move("Z") 179 | 180 | def place_middle_layer_ld_edge(self, ld_piece, left_color, down_color): 181 | # move to z == -1 182 | if ld_piece.pos.z == 0: 183 | count = 0 184 | while (ld_piece.pos.x, ld_piece.pos.y) != (-1, -1): 185 | self.move("Z") 186 | count += 1 187 | 188 | self.move("B L Bi Li Bi Di B D") 189 | for _ in range(count): 190 | self.move("Zi") 191 | 192 | assert ld_piece.pos.z == -1 193 | 194 | if ld_piece.colors[2] == left_color: 195 | # left_color is on the back face, move piece to to down face 196 | while ld_piece.pos.y != -1: 197 | self.move("B") 198 | self.move("B L Bi Li Bi Di B D") 199 | elif ld_piece.colors[2] == down_color: 200 | # down_color is on the back face, move to left face 201 | while ld_piece.pos.x != -1: 202 | self.move("B") 203 | self.move("Bi Di B D B L Bi Li") 204 | else: 205 | raise Exception("BUG!!") 206 | 207 | def back_face_edges(self): 208 | # rotate BACK to FRONT 209 | self.move("X X") 210 | 211 | # States: 1 2 3 4 212 | # -B- -B- --- --- 213 | # BBB BB- BBB -B- 214 | # -B- --- --- --- 215 | def state1(): 216 | return (self.cube[0, 1, 1].colors[2] == self.cube.front_color() and 217 | self.cube[-1, 0, 1].colors[2] == self.cube.front_color() and 218 | self.cube[0, -1, 1].colors[2] == self.cube.front_color() and 219 | self.cube[1, 0, 1].colors[2] == self.cube.front_color()) 220 | 221 | def state2(): 222 | return (self.cube[0, 1, 1].colors[2] == self.cube.front_color() and 223 | self.cube[-1, 0, 1].colors[2] == self.cube.front_color()) 224 | 225 | def state3(): 226 | return (self.cube[-1, 0, 1].colors[2] == self.cube.front_color() and 227 | self.cube[1, 0, 1].colors[2] == self.cube.front_color()) 228 | 229 | def state4(): 230 | return (self.cube[0, 1, 1].colors[2] != self.cube.front_color() and 231 | self.cube[-1, 0, 1].colors[2] != self.cube.front_color() and 232 | self.cube[0, -1, 1].colors[2] != self.cube.front_color() and 233 | self.cube[1, 0, 1].colors[2] != self.cube.front_color()) 234 | 235 | count = 0 236 | while not state1(): 237 | if state4() or state2(): 238 | self.move("D F R Fi Ri Di") 239 | elif state3(): 240 | self.move("D R F Ri Fi Di") 241 | else: 242 | self.move("F") 243 | count += 1 244 | if count >= self.inifinite_loop_max_iterations: 245 | raise Exception("Stuck in loop - unsolvable cube\n" + str(self.cube)) 246 | 247 | self.move("Xi Xi") 248 | 249 | def last_layer_corners_position(self): 250 | self.move("X X") 251 | # UP face: 252 | # 4-3 253 | # --- 254 | # 2-1 255 | move_1 = "Li Fi L D F Di Li F L F F " # swaps 1 and 2 256 | move_2 = "F Li Fi L D F Di Li F L F " # swaps 1 and 3 257 | 258 | c1 = self.cube.find_piece(self.cube.front_color(), self.cube.right_color(), self.cube.down_color()) 259 | c2 = self.cube.find_piece(self.cube.front_color(), self.cube.left_color(), self.cube.down_color()) 260 | c3 = self.cube.find_piece(self.cube.front_color(), self.cube.right_color(), self.cube.up_color()) 261 | c4 = self.cube.find_piece(self.cube.front_color(), self.cube.left_color(), self.cube.up_color()) 262 | 263 | # place corner 4 264 | if c4.pos == Point(1, -1, 1): 265 | self.move(move_1 + "Zi " + move_1 + " Z") 266 | elif c4.pos == Point(1, 1, 1): 267 | self.move("Z " + move_2 + " Zi") 268 | elif c4.pos == Point(-1, -1, 1): 269 | self.move("Zi " + move_1 + " Z") 270 | assert c4.pos == Point(-1, 1, 1) 271 | 272 | # place corner 2 273 | if c2.pos == Point(1, 1, 1): 274 | self.move(move_2 + move_1) 275 | elif c2.pos == Point(1, -1, 1): 276 | self.move(move_1) 277 | assert c2.pos == Point(-1, -1, 1) 278 | 279 | # place corner 3 and corner 1 280 | if c3.pos == Point(1, -1, 1): 281 | self.move(move_2) 282 | assert c3.pos == Point(1, 1, 1) 283 | assert c1.pos == Point(1, -1, 1) 284 | 285 | self.move("Xi Xi") 286 | 287 | def last_layer_corners_orientation(self): 288 | self.move("X X") 289 | 290 | # States: 1 2 3 4 5 6 7 8 291 | # B B B B B 292 | # BB- -B-B BBB -BB -BB B-B- B-B-B BBB 293 | # BBB BBB BBB BBB BBB BBB BBB BBB 294 | # -B-B BB- -B- -BB BB-B B-B- B-B-B BBB 295 | # B B B B B B 296 | def state1(): 297 | return (self.cube[ 1, 1, 1].colors[1] == self.cube.front_color() and 298 | self.cube[-1, -1, 1].colors[1] == self.cube.front_color() and 299 | self.cube[ 1, -1, 1].colors[0] == self.cube.front_color()) 300 | 301 | def state2(): 302 | return (self.cube[-1, 1, 1].colors[1] == self.cube.front_color() and 303 | self.cube[ 1, 1, 1].colors[0] == self.cube.front_color() and 304 | self.cube[ 1, -1, 1].colors[1] == self.cube.front_color()) 305 | 306 | def state3(): 307 | return (self.cube[-1, -1, 1].colors[1] == self.cube.front_color() and 308 | self.cube[ 1, -1, 1].colors[1] == self.cube.front_color() and 309 | self.cube[-1, 1, 1].colors[2] == self.cube.front_color() and 310 | self.cube[ 1, 1, 1].colors[2] == self.cube.front_color()) 311 | 312 | def state4(): 313 | return (self.cube[-1, 1, 1].colors[1] == self.cube.front_color() and 314 | self.cube[-1, -1, 1].colors[1] == self.cube.front_color() and 315 | self.cube[ 1, 1, 1].colors[2] == self.cube.front_color() and 316 | self.cube[ 1, -1, 1].colors[2] == self.cube.front_color()) 317 | 318 | def state5(): 319 | return (self.cube[-1, 1, 1].colors[1] == self.cube.front_color() and 320 | self.cube[ 1, -1, 1].colors[0] == self.cube.front_color()) 321 | 322 | def state6(): 323 | return (self.cube[ 1, 1, 1].colors[1] == self.cube.front_color() and 324 | self.cube[ 1, -1, 1].colors[1] == self.cube.front_color() and 325 | self.cube[-1, -1, 1].colors[0] == self.cube.front_color() and 326 | self.cube[-1, 1, 1].colors[0] == self.cube.front_color()) 327 | 328 | def state7(): 329 | return (self.cube[ 1, 1, 1].colors[0] == self.cube.front_color() and 330 | self.cube[ 1, -1, 1].colors[0] == self.cube.front_color() and 331 | self.cube[-1, -1, 1].colors[0] == self.cube.front_color() and 332 | self.cube[-1, 1, 1].colors[0] == self.cube.front_color()) 333 | 334 | def state8(): 335 | return (self.cube[ 1, 1, 1].colors[2] == self.cube.front_color() and 336 | self.cube[ 1, -1, 1].colors[2] == self.cube.front_color() and 337 | self.cube[-1, -1, 1].colors[2] == self.cube.front_color() and 338 | self.cube[-1, 1, 1].colors[2] == self.cube.front_color()) 339 | 340 | move_1 = "Ri Fi R Fi Ri F F R F F " 341 | move_2 = "R F Ri F R F F Ri F F " 342 | 343 | count = 0 344 | while not state8(): 345 | if state1(): self.move(move_1) 346 | elif state2(): self.move(move_2) 347 | elif state3(): self.move(move_2 + "F F " + move_1) 348 | elif state4(): self.move(move_2 + move_1) 349 | elif state5(): self.move(move_1 + "F " + move_2) 350 | elif state6(): self.move(move_1 + "Fi " + move_1) 351 | elif state7(): self.move(move_1 + "F F " + move_1) 352 | else: 353 | self.move("F") 354 | 355 | count += 1 356 | if count >= self.inifinite_loop_max_iterations: 357 | raise Exception("Stuck in loop - unsolvable cube:\n" + str(self.cube)) 358 | 359 | # rotate corners into correct locations (cube is inverted, so swap up and down colors) 360 | bru_corner = self.cube.find_piece(self.cube.front_color(), self.cube.right_color(), self.cube.up_color()) 361 | while bru_corner.pos != Point(1, 1, 1): 362 | self.move("F") 363 | 364 | self.move("Xi Xi") 365 | 366 | def last_layer_edges(self): 367 | self.move("X X") 368 | 369 | br_edge = self.cube.find_piece(self.cube.front_color(), self.cube.right_color()) 370 | bl_edge = self.cube.find_piece(self.cube.front_color(), self.cube.left_color()) 371 | bu_edge = self.cube.find_piece(self.cube.front_color(), self.cube.up_color()) 372 | bd_edge = self.cube.find_piece(self.cube.front_color(), self.cube.down_color()) 373 | 374 | # States: 375 | # 1 2 376 | # --- --- 377 | # --- --- 378 | # -B- -a- 379 | # --- B-B --- aaa BBB --- 380 | # --B -B- B-- aaB -B- B-- 381 | # --- B-B --- aaa B-B --- 382 | # -B- -B- 383 | # --- --- 384 | # --- --- 385 | # (aB edge on any FRONT) 386 | def state1(): 387 | return (bu_edge.colors[2] != self.cube.front_color() and 388 | bd_edge.colors[2] != self.cube.front_color() and 389 | bl_edge.colors[2] != self.cube.front_color() and 390 | br_edge.colors[2] != self.cube.front_color()) 391 | 392 | def state2(): 393 | return (bu_edge.colors[2] == self.cube.front_color() or 394 | bd_edge.colors[2] == self.cube.front_color() or 395 | bl_edge.colors[2] == self.cube.front_color() or 396 | br_edge.colors[2] == self.cube.front_color()) 397 | 398 | 399 | cycle_move = "R R F D Ui R R Di U F R R" 400 | h_pattern_move = "Ri S Ri Ri S S Ri Fi Fi R Si Si Ri Ri Si R Fi Fi " 401 | fish_move = "Di Li " + h_pattern_move + " L D" 402 | 403 | if state1(): 404 | # ideally, convert state1 into state2 405 | self._handle_last_layer_state1(br_edge, bl_edge, bu_edge, bd_edge, cycle_move, h_pattern_move) 406 | if state2(): 407 | self._handle_last_layer_state2(br_edge, bl_edge, bu_edge, bd_edge, cycle_move) 408 | 409 | def h_pattern1(): 410 | return (self.cube[-1, 0, 1].colors[0] != self.cube.left_color() and 411 | self.cube[ 1, 0, 1].colors[0] != self.cube.right_color() and 412 | self.cube[ 0, -1, 1].colors[1] == self.cube.down_color() and 413 | self.cube[ 0, 1, 1].colors[1] == self.cube.up_color()) 414 | 415 | def h_pattern2(): 416 | return (self.cube[-1, 0, 1].colors[0] == self.cube.left_color() and 417 | self.cube[ 1, 0, 1].colors[0] == self.cube.right_color() and 418 | self.cube[ 0, -1, 1].colors[1] == self.cube.front_color() and 419 | self.cube[ 0, 1, 1].colors[1] == self.cube.front_color()) 420 | 421 | def fish_pattern(): 422 | return (self.cube[cube.FRONT + cube.DOWN].colors[2] == self.cube.down_color() and 423 | self.cube[cube.FRONT + cube.RIGHT].colors[2] == self.cube.right_color() and 424 | self.cube[cube.FRONT + cube.DOWN].colors[1] == self.cube.front_color() and 425 | self.cube[cube.FRONT + cube.RIGHT].colors[0] == self.cube.front_color()) 426 | 427 | count = 0 428 | while not self.cube.is_solved(): 429 | for _ in range(4): 430 | if fish_pattern(): 431 | self.move(fish_move) 432 | if self.cube.is_solved(): 433 | return 434 | else: 435 | self.move("Z") 436 | 437 | if h_pattern1(): 438 | self.move(h_pattern_move) 439 | elif h_pattern2(): 440 | self.move("Z " + h_pattern_move + "Zi") 441 | else: 442 | self.move(cycle_move) 443 | count += 1 444 | if count >= self.inifinite_loop_max_iterations: 445 | raise Exception("Stuck in loop - unsolvable cube:\n" + str(self.cube)) 446 | 447 | self.move("Xi Xi") 448 | 449 | 450 | def _handle_last_layer_state1(self, br_edge, bl_edge, bu_edge, bd_edge, cycle_move, h_move): 451 | if DEBUG: print("_handle_last_layer_state1") 452 | def check_edge_lr(): 453 | return self.cube[cube.LEFT + cube.FRONT].colors[2] == self.cube.left_color() 454 | 455 | count = 0 456 | while not check_edge_lr(): 457 | self.move("F") 458 | count += 1 459 | if count == 4: 460 | raise Exception("Bug: Failed to handle last layer state1") 461 | 462 | self.move(h_move) 463 | 464 | for _ in range(count): 465 | self.move("Fi") 466 | 467 | 468 | def _handle_last_layer_state2(self, br_edge, bl_edge, bu_edge, bd_edge, cycle_move): 469 | if DEBUG: print("_handle_last_layer_state2") 470 | def correct_edge(): 471 | piece = self.cube[cube.LEFT + cube.FRONT] 472 | if piece.colors[2] == self.cube.front_color() and piece.colors[0] == self.cube.left_color(): 473 | return piece 474 | piece = self.cube[cube.RIGHT + cube.FRONT] 475 | if piece.colors[2] == self.cube.front_color() and piece.colors[0] == self.cube.right_color(): 476 | return piece 477 | piece = self.cube[cube.UP + cube.FRONT] 478 | if piece.colors[2] == self.cube.front_color() and piece.colors[1] == self.cube.up_color(): 479 | return piece 480 | piece = self.cube[cube.DOWN + cube.FRONT] 481 | if piece.colors[2] == self.cube.front_color() and piece.colors[1] == self.cube.down_color(): 482 | return piece 483 | 484 | count = 0 485 | while True: 486 | edge = correct_edge() 487 | if edge is None: 488 | self.move(cycle_move) 489 | else: 490 | break 491 | 492 | count += 1 493 | 494 | if count % 3 == 0: 495 | self.move("Z") 496 | 497 | if count >= self.inifinite_loop_max_iterations: 498 | raise Exception("Stuck in loop - unsolvable cube:\n" + str(self.cube)) 499 | 500 | while edge.pos != Point(-1, 0, 1): 501 | self.move("Z") 502 | 503 | assert self.cube[cube.LEFT + cube.FRONT].colors[2] == self.cube.front_color() and \ 504 | self.cube[cube.LEFT + cube.FRONT].colors[0] == self.cube.left_color() 505 | 506 | 507 | if __name__ == '__main__': 508 | DEBUG = True 509 | c = cube.Cube("DLURRDFFUBBLDDRBRBLDLRBFRUULFBDDUFBRBBRFUDFLUDLUULFLFR") 510 | print("Solving:\n", c) 511 | orig = cube.Cube(c) 512 | solver = Solver(c) 513 | solver.solve() 514 | 515 | print(f"{len(solver.moves)} moves: {' '.join(solver.moves)}") 516 | 517 | check = cube.Cube(orig) 518 | check.sequence(" ".join(solver.moves)) 519 | assert check.is_solved() 520 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import string 2 | import unittest 3 | import itertools 4 | import traceback 5 | 6 | import rubik.cube as cube 7 | from rubik.cube import Cube 8 | from rubik.maths import Point, Matrix 9 | from rubik.solve import Solver 10 | from rubik.optimize import optimize_moves 11 | import rubik.optimize 12 | 13 | solved_cube_str = \ 14 | """ UUU 15 | UUU 16 | UUU 17 | LLL FFF RRR BBB 18 | LLL FFF RRR BBB 19 | LLL FFF RRR BBB 20 | DDD 21 | DDD 22 | DDD""" 23 | 24 | debug_cube_str = \ 25 | """ 012 26 | 345 27 | 678 28 | 9ab cde fgh ijk 29 | lmn opq rst uvw 30 | xyz ABC DEF GHI 31 | JKL 32 | MNO 33 | PQR""" 34 | 35 | 36 | class TestPoint(unittest.TestCase): 37 | 38 | def setUp(self): 39 | self.p = Point(1, 2, 3) 40 | self.q = Point(2, 5, 9) 41 | self.r = Point(2, 2, 3) 42 | 43 | def test_point_constructor(self): 44 | self.assertEqual(self.p.x, 1) 45 | self.assertEqual(self.p.y, 2) 46 | self.assertEqual(self.p.z, 3) 47 | 48 | def test_point_count(self): 49 | self.assertEqual(self.r.count(2), 2) 50 | self.assertEqual(self.r.count(3), 1) 51 | self.assertEqual(self.r.count(5), 0) 52 | self.assertEqual(Point(9, 9, 9).count(9), 3) 53 | 54 | def test_point_eq(self): 55 | pp = Point(self.p.x, self.p.y, self.p.z) 56 | self.assertEqual(self.p, pp) 57 | self.assertTrue(self.p == pp) 58 | self.assertTrue(self.p == (1, 2, 3)) 59 | self.assertTrue(self.p == [1, 2, 3]) 60 | 61 | def test_point_neq(self): 62 | points = [Point(1, 2, 3), Point(1, 2, 4), Point(1, 3, 3), Point(2, 2, 3)] 63 | for i in range(len(points)): 64 | for j in range(i + 1, len(points)): 65 | self.assertNotEqual(points[i], points[j]) 66 | self.assertTrue(points[i] != points[j]) 67 | self.assertFalse(points[i] == points[j]) 68 | 69 | def test_point_add(self): 70 | self.assertEqual(self.p + self.q, Point(3, 7, 12)) 71 | 72 | def test_point_iadd(self): 73 | self.p += self.q 74 | self.assertEqual(self.p, Point(3, 7, 12)) 75 | 76 | def test_point_sub(self): 77 | self.assertEqual(self.p - self.q, Point(-1, -3, -6)) 78 | 79 | def test_point_isub(self): 80 | self.p -= self.q 81 | self.assertEqual(self.p, Point(-1, -3, -6)) 82 | 83 | def test_point_scale(self): 84 | self.assertEqual(self.p * 3, Point(3, 6, 9)) 85 | 86 | def test_point_dot_product(self): 87 | self.assertEqual(self.p.dot(self.q), 39) 88 | 89 | def test_point_cross_produce(self): 90 | self.assertEqual(self.p.cross(self.q), Point(3, -3, 1)) 91 | 92 | def test_to_tuple(self): 93 | self.assertEqual(tuple(self.p), (1, 2, 3)) 94 | 95 | def test_to_list(self): 96 | self.assertEqual(list(self.p), [1, 2, 3]) 97 | 98 | def test_from_tuple(self): 99 | self.assertEqual(Point((1, 2, 3)), self.p) 100 | 101 | def test_from_list(self): 102 | self.assertEqual(Point([1, 2, 3]), self.p) 103 | 104 | def test_point_str(self): 105 | self.assertEqual(str(self.p), "(1, 2, 3)") 106 | 107 | def test_point_repr(self): 108 | self.assertEqual(repr(self.p), "Point(1, 2, 3)") 109 | 110 | def test_point_iter(self): 111 | ii = iter(self.p) 112 | self.assertEqual(1, next(ii)) 113 | self.assertEqual(2, next(ii)) 114 | self.assertEqual(3, next(ii)) 115 | self.assertRaises(StopIteration, ii.__next__) 116 | 117 | def test_point_getitem(self): 118 | self.assertEqual(self.p[0], 1) 119 | self.assertEqual(self.p[1], 2) 120 | self.assertEqual(self.p[2], 3) 121 | 122 | 123 | class TestMatrix(unittest.TestCase): 124 | 125 | def setUp(self): 126 | self.A = Matrix(1, 2, 3, 127 | 4, 5, 6, 128 | 7, 8, 9) 129 | self.B = Matrix([9, 8, 7, 130 | 6, 5, 4, 131 | 3, 2, 1]) 132 | self.C = Matrix([[3, 2, 1], 133 | [6, 5, 4], 134 | [9, 8, 7]]) 135 | 136 | self.D = Matrix(x for x in range(11, 20)) 137 | self.p = Point(1, 2, 3) 138 | 139 | def test_matrix_from_items(self): 140 | self.assertEqual(self.A.vals, list(range(1, 10))) 141 | 142 | def test_matrix_from_list(self): 143 | self.assertEqual(self.B.vals, list(range(9, 0, -1))) 144 | 145 | def test_matrix_from_nested_lists(self): 146 | self.assertEqual(self.C.vals, [3, 2, 1, 6, 5, 4, 9, 8, 7]) 147 | 148 | def test_matrix_from_iterable(self): 149 | self.assertEqual(self.D.vals, list(range(11, 20))) 150 | 151 | def test_matrix_str(self): 152 | self.assertEqual(str(self.A), ("[1, 2, 3,\n" 153 | " 4, 5, 6,\n" 154 | " 7, 8, 9]")) 155 | 156 | def test_matrix_repr(self): 157 | self.assertEqual(repr(self.A), ("Matrix(1, 2, 3,\n" 158 | " 4, 5, 6,\n" 159 | " 7, 8, 9)")) 160 | 161 | def test_matrix_eq(self): 162 | self.assertEqual(self.A, self.A) 163 | self.assertEqual(self.A, Matrix(list(range(1, 10)))) 164 | 165 | def test_matrix_add(self): 166 | self.assertEqual(self.A + self.D, Matrix(12, 14, 16, 18, 20, 22, 24, 26, 28)) 167 | 168 | def test_matrix_sub(self): 169 | self.assertEqual(self.A - self.B, Matrix(-8, -6, -4, -2, 0, 2, 4, 6, 8)) 170 | 171 | def test_matrix_iadd(self): 172 | self.A += self.D 173 | self.assertEqual(self.A, Matrix(12, 14, 16, 18, 20, 22, 24, 26, 28)) 174 | 175 | def test_matrix_isub(self): 176 | self.A -= self.B 177 | self.assertEqual(self.A, Matrix(-8, -6, -4, -2, 0, 2, 4, 6, 8)) 178 | 179 | def test_matrix_rows(self): 180 | self.assertEqual(list(self.A.rows()), [[1, 2, 3], [4, 5, 6], [7, 8, 9]]) 181 | 182 | def test_matrix_cols(self): 183 | self.assertEqual(list(self.A.cols()), [[1, 4, 7], [2, 5, 8], [3, 6, 9]]) 184 | 185 | def test_matrix_point_mul(self): 186 | self.assertEqual(self.A * self.p, Point(14, 32, 50)) 187 | 188 | def test_matrix_matrix_mul(self): 189 | self.assertEqual(self.A * self.B, Matrix(30, 24, 18, 84, 69, 54, 138, 114, 90)) 190 | 191 | 192 | class TestCube(unittest.TestCase): 193 | 194 | def setUp(self): 195 | self.solved_cube = Cube(solved_cube_str) 196 | self.debug_cube = Cube(debug_cube_str) 197 | 198 | def test_cube_constructor_solved_cube(self): 199 | self.assertEqual(solved_cube_str, str(self.solved_cube)) 200 | 201 | def test_cube_constructor_unique_stickers(self): 202 | self.assertEqual(debug_cube_str, str(self.debug_cube)) 203 | 204 | def test_cube_constructor_additional_whitespace(self): 205 | cube = Cube(" ".join(x for x in debug_cube_str)) 206 | self.assertEqual(debug_cube_str, str(cube)) 207 | 208 | def test_cube_copy_constructor(self): 209 | c = Cube(self.debug_cube) 210 | self.assertEqual(debug_cube_str, str(c)) 211 | self.assertEqual(self.debug_cube, c) 212 | 213 | def test_cube_eq(self): 214 | c = Cube(debug_cube_str) 215 | self.assertEqual(c, self.debug_cube) 216 | self.assertEqual(self.debug_cube, c) 217 | 218 | def test_cube_neq(self): 219 | c = Cube(debug_cube_str) 220 | c.L() 221 | self.assertNotEqual(c, self.debug_cube) 222 | self.assertNotEqual(self.debug_cube, c) 223 | self.assertFalse(c == self.debug_cube) 224 | self.assertTrue(c != self.debug_cube) 225 | 226 | def test_cube_constructor_no_whitespace(self): 227 | cube = Cube("".join(x for x in debug_cube_str if x not in string.whitespace)) 228 | self.assertEqual(debug_cube_str, str(cube)) 229 | 230 | def test_cube_L(self): 231 | self.debug_cube.L() 232 | self.assertEqual(" I12\n" 233 | " w45\n" 234 | " k78\n" 235 | "xl9 0de fgh ijP\n" 236 | "yma 3pq rst uvM\n" 237 | "znb 6BC DEF GHJ\n" 238 | " cKL\n" 239 | " oNO\n" 240 | " AQR", 241 | str(self.debug_cube)) 242 | 243 | def test_cube_Li(self): 244 | self.debug_cube.Li() 245 | self.assertEqual(" c12\n" 246 | " o45\n" 247 | " A78\n" 248 | "bnz Jde fgh ij6\n" 249 | "amy Mpq rst uv3\n" 250 | "9lx PBC DEF GH0\n" 251 | " IKL\n" 252 | " wNO\n" 253 | " kQR", 254 | str(self.debug_cube)) 255 | 256 | def test_cube_R(self): 257 | self.debug_cube.R() 258 | self.assertEqual(" 01e\n" 259 | " 34q\n" 260 | " 67C\n" 261 | "9ab cdL Drf 8jk\n" 262 | "lmn opO Esg 5vw\n" 263 | "xyz ABR Fth 2HI\n" 264 | " JKG\n" 265 | " MNu\n" 266 | " PQi", 267 | str(self.debug_cube)) 268 | 269 | def test_cube_Ri(self): 270 | self.debug_cube.Ri() 271 | self.assertEqual(" 01G\n" 272 | " 34u\n" 273 | " 67i\n" 274 | "9ab cd2 htF Rjk\n" 275 | "lmn op5 gsE Ovw\n" 276 | "xyz AB8 frD LHI\n" 277 | " JKe\n" 278 | " MNq\n" 279 | " PQC", 280 | str(self.debug_cube)) 281 | 282 | def test_cube_U(self): 283 | self.debug_cube.U() 284 | self.assertEqual(" 630\n" 285 | " 741\n" 286 | " 852\n" 287 | "cde fgh ijk 9ab\n" 288 | "lmn opq rst uvw\n" 289 | "xyz ABC DEF GHI\n" 290 | " JKL\n" 291 | " MNO\n" 292 | " PQR", 293 | str(self.debug_cube)) 294 | 295 | def test_cube_Ui(self): 296 | self.debug_cube.Ui() 297 | self.assertEqual(" 258\n" 298 | " 147\n" 299 | " 036\n" 300 | "ijk 9ab cde fgh\n" 301 | "lmn opq rst uvw\n" 302 | "xyz ABC DEF GHI\n" 303 | " JKL\n" 304 | " MNO\n" 305 | " PQR", 306 | str(self.debug_cube)) 307 | 308 | def test_cube_D(self): 309 | self.debug_cube.D() 310 | self.assertEqual(" 012\n" 311 | " 345\n" 312 | " 678\n" 313 | "9ab cde fgh ijk\n" 314 | "lmn opq rst uvw\n" 315 | "GHI xyz ABC DEF\n" 316 | " PMJ\n" 317 | " QNK\n" 318 | " ROL", 319 | str(self.debug_cube)) 320 | 321 | def test_cube_Di(self): 322 | self.debug_cube.Di() 323 | self.assertEqual(" 012\n" 324 | " 345\n" 325 | " 678\n" 326 | "9ab cde fgh ijk\n" 327 | "lmn opq rst uvw\n" 328 | "ABC DEF GHI xyz\n" 329 | " LOR\n" 330 | " KNQ\n" 331 | " JMP", 332 | str(self.debug_cube)) 333 | 334 | def test_cube_F(self): 335 | self.debug_cube.F() 336 | self.assertEqual(" 012\n" 337 | " 345\n" 338 | " znb\n" 339 | "9aJ Aoc 6gh ijk\n" 340 | "lmK Bpd 7st uvw\n" 341 | "xyL Cqe 8EF GHI\n" 342 | " Drf\n" 343 | " MNO\n" 344 | " PQR", 345 | str(self.debug_cube)) 346 | 347 | def test_cube_Fi(self): 348 | self.debug_cube.Fi() 349 | self.assertEqual(" 012\n" 350 | " 345\n" 351 | " frD\n" 352 | "9a8 eqC Lgh ijk\n" 353 | "lm7 dpB Kst uvw\n" 354 | "xy6 coA JEF GHI\n" 355 | " bnz\n" 356 | " MNO\n" 357 | " PQR", 358 | str(self.debug_cube)) 359 | 360 | def test_cube_B(self): 361 | self.debug_cube.B() 362 | self.assertEqual(" htF\n" 363 | " 345\n" 364 | " 678\n" 365 | "2ab cde fgR Gui\n" 366 | "1mn opq rsQ Hvj\n" 367 | "0yz ABC DEP Iwk\n" 368 | " JKL\n" 369 | " MNO\n" 370 | " 9lx", 371 | str(self.debug_cube)) 372 | 373 | def test_cube_Bi(self): 374 | self.debug_cube.Bi() 375 | self.assertEqual(" xl9\n" 376 | " 345\n" 377 | " 678\n" 378 | "Pab cde fg0 kwI\n" 379 | "Qmn opq rs1 jvH\n" 380 | "Ryz ABC DE2 iuG\n" 381 | " JKL\n" 382 | " MNO\n" 383 | " Fth", 384 | str(self.debug_cube)) 385 | 386 | def test_cube_M(self): 387 | self.debug_cube.M() 388 | self.assertEqual(" 0H2\n" 389 | " 3v5\n" 390 | " 6j8\n" 391 | "9ab c1e fgh iQk\n" 392 | "lmn o4q rst uNw\n" 393 | "xyz A7C DEF GKI\n" 394 | " JdL\n" 395 | " MpO\n" 396 | " PBR", 397 | str(self.debug_cube)) 398 | 399 | def test_cube_Mi(self): 400 | self.debug_cube.Mi() 401 | self.assertEqual(" 0d2\n" 402 | " 3p5\n" 403 | " 6B8\n" 404 | "9ab cKe fgh i7k\n" 405 | "lmn oNq rst u4w\n" 406 | "xyz AQC DEF G1I\n" 407 | " JHL\n" 408 | " MvO\n" 409 | " PjR", 410 | str(self.debug_cube)) 411 | 412 | def test_cube_E(self): 413 | self.debug_cube.E() 414 | self.assertEqual(" 012\n" 415 | " 345\n" 416 | " 678\n" 417 | "9ab cde fgh ijk\n" 418 | "uvw lmn opq rst\n" 419 | "xyz ABC DEF GHI\n" 420 | " JKL\n" 421 | " MNO\n" 422 | " PQR", 423 | str(self.debug_cube)) 424 | 425 | def test_cube_Ei(self): 426 | self.debug_cube.Ei() 427 | self.assertEqual(" 012\n" 428 | " 345\n" 429 | " 678\n" 430 | "9ab cde fgh ijk\n" 431 | "opq rst uvw lmn\n" 432 | "xyz ABC DEF GHI\n" 433 | " JKL\n" 434 | " MNO\n" 435 | " PQR", 436 | str(self.debug_cube)) 437 | 438 | def test_cube_S(self): 439 | self.debug_cube.S() 440 | self.assertEqual(" 012\n" 441 | " yma\n" 442 | " 678\n" 443 | "9Mb cde f3h ijk\n" 444 | "lNn opq r4t uvw\n" 445 | "xOz ABC D5F GHI\n" 446 | " JKL\n" 447 | " Esg\n" 448 | " PQR", 449 | str(self.debug_cube)) 450 | 451 | def test_cube_Si(self): 452 | self.debug_cube.Si() 453 | self.assertEqual(" 012\n" 454 | " gsE\n" 455 | " 678\n" 456 | "95b cde fOh ijk\n" 457 | "l4n opq rNt uvw\n" 458 | "x3z ABC DMF GHI\n" 459 | " JKL\n" 460 | " amy\n" 461 | " PQR", 462 | str(self.debug_cube)) 463 | 464 | def test_cube_X(self): 465 | self.debug_cube.X() 466 | self.assertEqual(" cde\n" 467 | " opq\n" 468 | " ABC\n" 469 | "bnz JKL Drf 876\n" 470 | "amy MNO Esg 543\n" 471 | "9lx PQR Fth 210\n" 472 | " IHG\n" 473 | " wvu\n" 474 | " kji", 475 | str(self.debug_cube)) 476 | 477 | def test_cube_Xi(self): 478 | self.debug_cube.Xi() 479 | self.assertEqual(" IHG\n" 480 | " wvu\n" 481 | " kji\n" 482 | "xl9 012 htF RQP\n" 483 | "yma 345 gsE ONM\n" 484 | "znb 678 frD LKJ\n" 485 | " cde\n" 486 | " opq\n" 487 | " ABC", 488 | str(self.debug_cube)) 489 | 490 | def test_cube_Y(self): 491 | self.debug_cube.Y() 492 | self.assertEqual(" 630\n" 493 | " 741\n" 494 | " 852\n" 495 | "cde fgh ijk 9ab\n" 496 | "opq rst uvw lmn\n" 497 | "ABC DEF GHI xyz\n" 498 | " LOR\n" 499 | " KNQ\n" 500 | " JMP", 501 | str(self.debug_cube)) 502 | 503 | def test_cube_Yi(self): 504 | self.debug_cube.Yi() 505 | self.assertEqual(" 258\n" 506 | " 147\n" 507 | " 036\n" 508 | "ijk 9ab cde fgh\n" 509 | "uvw lmn opq rst\n" 510 | "GHI xyz ABC DEF\n" 511 | " PMJ\n" 512 | " QNK\n" 513 | " ROL", 514 | str(self.debug_cube)) 515 | 516 | def test_cube_Z(self): 517 | self.debug_cube.Z() 518 | self.assertEqual(" xl9\n" 519 | " yma\n" 520 | " znb\n" 521 | "PMJ Aoc 630 kwI\n" 522 | "QNK Bpd 741 jvH\n" 523 | "ROL Cqe 852 iuG\n" 524 | " Drf\n" 525 | " Esg\n" 526 | " Fth", 527 | str(self.debug_cube)) 528 | 529 | def test_cube_Zi(self): 530 | self.debug_cube.Zi() 531 | self.assertEqual(" htF\n" 532 | " gsE\n" 533 | " frD\n" 534 | "258 eqC LOR Gui\n" 535 | "147 dpB KNQ Hvj\n" 536 | "036 coA JMP Iwk\n" 537 | " bnz\n" 538 | " amy\n" 539 | " 9lx", 540 | str(self.debug_cube)) 541 | 542 | def test_cube_find_face_piece(self): 543 | piece = self.debug_cube.find_piece('p') 544 | self.assertEqual(cube.FACE, piece.type) 545 | self.assertEqual(cube.FRONT, piece.pos) 546 | self.assertEqual([None, None, 'p'], piece.colors) 547 | 548 | def test_cube_find_edge_piece(self): 549 | def _check_piece(piece): 550 | self.assertEqual(cube.EDGE, piece.type) 551 | self.assertEqual(cube.FRONT + cube.UP, piece.pos) 552 | self.assertEqual([None, '7', 'd'], piece.colors) 553 | _check_piece(self.debug_cube.find_piece('d', '7')) 554 | _check_piece(self.debug_cube.find_piece('7', 'd')) 555 | 556 | def test_cube_find_corner_piece(self): 557 | def _check_piece(piece): 558 | self.assertEqual(cube.CORNER, piece.type) 559 | self.assertEqual(cube.FRONT + cube.UP + cube.LEFT, piece.pos) 560 | self.assertEqual(['b', '6', 'c'], piece.colors) 561 | for colors in itertools.permutations(('b', '6', 'c')): 562 | _check_piece(self.debug_cube.find_piece(*colors)) 563 | 564 | def test_cube_find_face_piece_negative(self): 565 | self.assertIsNone(self.debug_cube.find_piece('7')) 566 | 567 | def test_cube_find_edge_piece_negative(self): 568 | self.assertIsNone(self.debug_cube.find_piece('o', 'q')) 569 | 570 | def test_cube_find_corner_piece_negative(self): 571 | self.assertIsNone(self.debug_cube.find_piece('c', '6', '9')) 572 | 573 | def test_cube_is_solved(self): 574 | self.assertTrue(self.solved_cube.is_solved()) 575 | 576 | def test_cube_is_solved_negative(self): 577 | self.solved_cube.L() 578 | self.assertFalse(self.solved_cube.is_solved()) 579 | self.assertFalse(self.debug_cube.is_solved()) 580 | 581 | def test_cube_sequence(self): 582 | self.solved_cube.sequence("L U M Ri X E Xi Ri D D F F Bi") 583 | self.assertEqual(" DLU\n" 584 | " RRD\n" 585 | " FFU\n" 586 | "BBL DDR BRB LDL\n" 587 | "RBF RUU LFB DDU\n" 588 | "FBR BBR FUD FLU\n" 589 | " DLU\n" 590 | " ULF\n" 591 | " LFR", 592 | str(self.solved_cube)) 593 | 594 | def test_cube_colors(self): 595 | self.assertEqual({'U', 'D', 'F', 'B', 'L', 'R'}, self.solved_cube.colors()) 596 | debug_colors = set() 597 | debug_colors.update(c for c in debug_cube_str if c not in string.whitespace) 598 | self.assertEqual(debug_colors, self.debug_cube.colors()) 599 | 600 | def test_cube_get_piece(self): 601 | piece = self.debug_cube.get_piece(0, 0, 1) 602 | self.assertEqual(cube.FACE, piece.type) 603 | self.assertEqual(cube.FRONT, piece.pos) 604 | 605 | def test_cube_getitem(self): 606 | piece = self.debug_cube[0, 0, 1] 607 | self.assertEqual(cube.FACE, piece.type) 608 | self.assertEqual(cube.FRONT, piece.pos) 609 | 610 | def test_cube_getitem_from_tuple(self): 611 | piece = self.debug_cube[(0, 0, 1)] 612 | self.assertEqual(cube.FACE, piece.type) 613 | self.assertEqual(cube.FRONT, piece.pos) 614 | 615 | def test_move_and_inverse(self): 616 | for name in ('R', 'L', 'U', 'D', 'F', 'B', 'M', 'E', 'S', 'X', 'Y', 'Z'): 617 | move, unmove = getattr(Cube, name), getattr(Cube, name + 'i') 618 | self._check_move_and_inverse(move, unmove, self.debug_cube) 619 | 620 | def _check_move_and_inverse(self, move, inverse, cube): 621 | check_str = str(cube) 622 | move(cube) 623 | self.assertNotEqual(check_str, str(cube)) 624 | inverse(cube) 625 | self.assertEqual(check_str, str(cube)) 626 | inverse(cube) 627 | self.assertNotEqual(check_str, str(cube)) 628 | move(cube) 629 | self.assertEqual(check_str, str(cube)) 630 | 631 | 632 | class TestSolver(unittest.TestCase): 633 | 634 | cubes = [ 635 | "DLURRDFFUBBLDDRBRBLDLRBFRUULFBDDUFBRBBRFUDFLUDLUULFLFR", 636 | "GGBYOBWBBBOYRGYOYOGWROWYWGWRBRGYBGOOGBBYOYORWWRRGRWRYW", 637 | "BYOYYRGOWRROWGOYWGBBGOROBWGWORBBWRWYRGYBGYWOGBROYGBWYR", 638 | "YWYYGWWGYBBYRRBRGWOOOYWRWRBOBYROWRGOBGRWOGWBBGBGOYYGRO", 639 | "ROORRYOWBWWGBYGRRBYBGGGGWWOYYBRBOWBYRWOGBYORYBOWYOGRGW" 640 | ] 641 | 642 | unsolvable_cubes = [ 643 | "ORWOWGWYWGBGRGRBOBOWYGGBRRBYBRGOWOYGRYRBBGOOBYOYRYWYWW", 644 | "UUUUUUUUULLLFFFRRRBBBLLLFBFRRRBFBLLLFFFRRRBBBDDDDDDDDD", 645 | "UUBUUUUUULLLFFFRRRUBBLLLFFFRRRBBBLLLFFFRRRBBBDDDDDDDDD", 646 | "UUUUUUUUULLLFFFRRRBBBLLLFFFRRRBBBLLLFFFRRBRBBDDDDDDDDD", 647 | "UUUUUUUUULLLFFFRRRBBBLLFLFFRRRBBBLLLFFFRRRBBBDDDDDDDDD", 648 | ] 649 | 650 | def test_cube_solver(self): 651 | for c in self.cubes: 652 | self._check_can_solve_cube(c) 653 | 654 | def _check_can_solve_cube(self, orig): 655 | c = Cube(orig) 656 | solver = Solver(c) 657 | try: 658 | solver.solve() 659 | self.assertTrue(c.is_solved(), msg="Failed to solve cube: " + orig) 660 | except Exception: 661 | self.fail(traceback.format_exc() + "original cube: " + orig) 662 | 663 | def test_unsolvable_cube(self): 664 | for c in self.unsolvable_cubes: 665 | self._check_cube_fails_to_solve(c) 666 | 667 | def _check_cube_fails_to_solve(self, orig): 668 | c = Cube(orig) 669 | solver = Solver(c) 670 | self.assertRaisesRegex(Exception, "Stuck in loop - unsolvable cube", solver.solve) 671 | 672 | class TestOptimize(unittest.TestCase): 673 | 674 | moves = (('R', 'Ri'), ('L', 'Li'), ('U', 'Ui'), ('D', 'Di'), ('F', 'Fi'), ('B', 'Bi'), 675 | ('M', 'Mi'), ('E', 'Ei'), ('S', 'Si'), ('X', 'Xi'), ('Y', 'Yi'), ('Z', 'Zi')) 676 | 677 | def test_optimize_repeat_three(self): 678 | for cw, cc in self.moves: 679 | self.assertEqual([cc], optimize_moves([cw, cw, cw])) 680 | self.assertEqual([cw], optimize_moves([cc, cc, cc])) 681 | self.assertEqual(['_', cw], optimize_moves(['_', cc, cc, cc])) 682 | self.assertEqual(['_', cc], optimize_moves(['_', cw, cw, cw])) 683 | self.assertEqual(['_', cw, '_'], optimize_moves(['_', cc, cc, cc, '_'])) 684 | self.assertEqual(['_', cc, '_'], optimize_moves(['_', cw, cw, cw, '_'])) 685 | 686 | self.assertEqual([cw, cw], 687 | optimize_moves([cc,cc,cc, cc,cc,cc])) 688 | self.assertEqual([cw, cw, '_'], 689 | optimize_moves([cc,cc,cc, cc,cc,cc, '_'])) 690 | self.assertEqual([cw, cw, '_', '_'], 691 | optimize_moves([cc,cc,cc, cc,cc,cc, '_','_'])) 692 | self.assertEqual([cc], 693 | optimize_moves([cc,cc,cc, cc,cc,cc, cc,cc,cc])) 694 | 695 | def test_optimize_do_undo(self): 696 | for cw, cc in self.moves: 697 | self.assertEqual([], optimize_moves([cc, cw])) 698 | self.assertEqual([], optimize_moves([cw, cc])) 699 | 700 | self.assertEqual([], optimize_moves([cw, cw, cc, cc])) 701 | self.assertEqual([], optimize_moves([cw, cw, cw, cc, cc, cc])) 702 | self.assertEqual([], optimize_moves([cw, cw, cw, cw, cc, cc, cc, cc])) 703 | 704 | self.assertEqual(['1', '2'], optimize_moves(['1', cw, cw, cc, cc, '2'])) 705 | self.assertEqual(['1', '2', '3', '4'], optimize_moves(['1', '2', cw, cw, cc, cc, '3', '4'])) 706 | 707 | def test_full_cube_rotation_optimization(self): 708 | for cw, cc in (('X', 'Xi'), ('Y', 'Yi'), ('Z', 'Zi')): 709 | for moves in ([cc, cw], [cw, cc]): 710 | rubik.optimize.apply_no_full_cube_rotation_optimization(moves) 711 | self.assertEqual([], moves) 712 | 713 | for cw, cc in (('Z', 'Zi'),): 714 | moves = [cw, 'U', 'L', 'D', 'R','E', 'M', cc] 715 | expected = ['L', 'D', 'R', 'U', 'Mi', 'E'] 716 | actual = list(moves) 717 | rubik.optimize.apply_no_full_cube_rotation_optimization(actual) 718 | self.assertEqual(expected, actual) 719 | 720 | c, d = Cube(solved_cube_str), Cube(solved_cube_str) 721 | c.sequence(" ".join(moves)) 722 | d.sequence(" ".join(actual)) 723 | self.assertEqual(str(c), str(d)) 724 | 725 | moves = [cw, cw, 'U', 'L', 'D', 'R','E', 'M', cc, cc] 726 | expected = ['D', 'R', 'U', 'L', 'Ei', 'Mi'] 727 | actual = list(moves) 728 | rubik.optimize.apply_no_full_cube_rotation_optimization(actual) 729 | self.assertEqual(expected, actual) 730 | 731 | c, d = Cube(solved_cube_str), Cube(solved_cube_str) 732 | c.sequence(" ".join(moves)) 733 | d.sequence(" ".join(actual)) 734 | self.assertEqual(str(c), str(d)) 735 | 736 | 737 | if __name__ == '__main__': 738 | unittest.main() 739 | --------------------------------------------------------------------------------