magiccube.cube
Rubik Cube implementation
1"""Rubik Cube implementation""" 2from typing import Dict, List, Optional, Tuple, Union 3import random 4import numpy as np 5from magiccube.cube_base import Color, CubeException, Face 6from magiccube.cube_piece import Coordinates, CubePiece 7from magiccube.cube_move import CubeMove, CubeMoveType 8from magiccube.cube_print import CubePrintStr 9 10 11class Cube: 12 """Rubik Cube implementation""" 13 14 __slots__ = ("size", "_store_history", "_cube_face_indexes", "_cube_piece_indexes", 15 "_cube_piece_indexes_inv", "_cube", "_history") 16 17 def __init__(self, size: int = 3, state: Optional[str] = None, hist: Optional[bool] = True): 18 19 if size <= 1: 20 raise CubeException("Cube size must be >= 2") 21 22 self.size = size 23 """Cube size""" 24 25 self._store_history = hist 26 27 # record the indexes of every cube face 28 self._cube_face_indexes = [ 29 [[(0, y, z) for z in range(self.size)] 30 for y in reversed(range(self.size))], # L 31 [[(self.size-1, y, z) for z in reversed(range(self.size))] 32 for y in reversed(range(self.size))], # R 33 [[(x, 0, z) for x in range(self.size)] 34 for z in reversed(range(self.size))], # D 35 [[(x, self.size-1, z) for x in range(self.size)] 36 for z in range(self.size)], # U 37 [[(x, y, 0) for x in reversed(range(self.size))] 38 for y in reversed(range(self.size))], # B 39 [[(x, y, self.size-1) for x in range(self.size)] 40 for y in reversed(range(self.size))], # F 41 ] 42 43 # record the indexes of every cube piece 44 self._cube_piece_indexes = [ 45 (x, y, z) 46 for z in range(self.size) 47 for y in range(self.size) 48 for x in range(self.size) 49 if self._is_outer_position(x, y, z) 50 ] 51 self._cube_piece_indexes_inv = { 52 v: idx for idx, v in enumerate(self._cube_piece_indexes)} 53 54 self.reset() 55 if state is not None: 56 self.set(state) 57 58 def _is_outer_position(self, _x: int, _y: int, _z: int) -> bool: 59 """Test if the coordinates indicate and outer cube position""" 60 return _x == 0 or _x == self.size-1 \ 61 or _y == 0 or _y == self.size-1 \ 62 or _z == 0 or _z == self.size-1 # dont include center pieces 63 64 def reset(self): 65 """Reset the cube to the initial configuration""" 66 initial_cube = [ 67 [[CubePiece(self.size, (x, y, z)) 68 if self._is_outer_position(x, y, z) else None 69 for x in range(self.size)] 70 for y in range(self.size)] 71 for z in range(self.size) 72 ] 73 self._cube = np.array(initial_cube, dtype=np.object_) 74 self._history = [] 75 76 def set(self, image: str): 77 """Sets the cube state. 78 79 Parameters 80 ---------- 81 image: str 82 Colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 83 Spaces and newlines are ignored. 84 85 Example: 86 YYYYYYYYY RRRRRRRRR GGGGGGGGG OOOOOOOOO BBBBBBBBB WWWWWWWWW 87 """ 88 image = image.replace(" ", "") 89 image = image.replace("\n", "") 90 91 if len(image) != 6*self.size*self.size: 92 raise CubeException( 93 "Cube state has an invalid size. Should be: " + str(6*self.size*self.size)) 94 95 img = [Color.create(x) for x in image] 96 97 self.reset() 98 for i, color in enumerate(img): 99 face = i // (self.size**2) 100 remain = i % (self.size**2) 101 if face == 0: # U 102 _x = remain % self.size 103 _y = self.size-1 104 _z = remain//self.size 105 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 106 elif face == 5: # D 107 _x = remain % self.size 108 _y = 0 109 _z = self.size-(remain//self.size)-1 110 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 111 elif face == 1: # L 112 _x = 0 113 _y = self.size-(remain//self.size)-1 114 _z = remain % self.size 115 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 116 elif face == 3: # R 117 _x = self.size-1 118 _y = self.size-(remain//self.size)-1 119 _z = self.size-(remain % self.size)-1 120 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 121 elif face == 4: # B 122 _x = self.size-(remain % self.size)-1 123 _y = self.size-(remain//self.size)-1 124 _z = 0 125 self.get_piece((_x, _y, _z)).set_piece_color(2, color) 126 elif face == 2: # F 127 _x = remain % self.size 128 _y = self.size-(remain//self.size)-1 129 _z = self.size-1 130 self.get_piece((_x, _y, _z)).set_piece_color(2, color) 131 132 def get(self, face_order: Optional[List[Face]] = None): 133 """ 134 Get the cube state as a string with the colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 135 136 Example: YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW 137 """ 138 139 if face_order is None: 140 face_order = [Face.U, Face.L, Face.F, Face.R, Face.B, Face.D] 141 142 res = [] 143 for face in face_order: 144 res += self.get_face_flat(face) 145 return "".join([x.name for x in res]) 146 147 def scramble(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 148 """Scramble the cube with random moves. 149 By default scramble only uses wide moves to cubes with size >=4.""" 150 151 movements = self.generate_random_moves(num_steps=num_steps, wide=wide) 152 self.rotate(movements) 153 return movements 154 155 def generate_random_moves(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 156 """Generate a list of random moves (but don't apply them). 157 By default scramble only uses wide moves to cubes with size >=4.""" 158 159 if wide is None and self.size <= 3: 160 wide = False 161 elif wide is None and self.size > 3: 162 wide = True 163 164 possible_moves = [ 165 CubeMoveType.L, CubeMoveType.R, # CubeMoveType.M, 166 CubeMoveType.D, CubeMoveType.U, # CubeMoveType.E, 167 CubeMoveType.B, CubeMoveType.F, # CubeMoveType.S, 168 ] 169 movements = [CubeMove( 170 random.choice(possible_moves), 171 random.choice([False, True]), # reversed 172 random.choice([False, True]) if wide else False, # wide 173 random.randint(1, self.size//2) if wide else 1 # layer 174 ) 175 for _ in range(num_steps)] 176 177 return movements 178 179 def find_piece(self, colors: str) -> Tuple[Coordinates, CubePiece]: 180 """Find the piece with given colors""" 181 colors = "".join(sorted(colors)) 182 for coord, piece in self.get_all_pieces().items(): 183 if colors == piece.get_piece_colors_str(no_loc=True): 184 return coord, piece 185 raise CubeException("piece not found " + colors) 186 187 def get_face(self, face: Face) -> List[List[Color]]: 188 """Get face colors in a multi-dim array""" 189 face_indexes = self._cube_face_indexes[face.value] 190 res = [] 191 for line in face_indexes: 192 line_color = [self._cube[index].get_piece_color( 193 face.get_axis()) for index in line] 194 res.append(line_color) 195 return res 196 197 def get_face_flat(self, face: Face) -> List[Color]: 198 """Get face colors in a flat array""" 199 res = self.get_face(face) 200 return list(np.array(res).flatten()) 201 202 def get_all_faces(self) -> Dict[Face, List[List[Color]]]: 203 """Get the CubePiece of all cube faces""" 204 faces = {f: self.get_face(f) for f in Face} 205 return faces 206 207 def get_piece(self, coordinates: Coordinates) -> CubePiece: 208 """Get the CubePiece at a given coordinate""" 209 return self._cube[coordinates] 210 211 def get_all_pieces(self) -> Dict[Coordinates, CubePiece]: 212 """Return a dictionary of coordinates:CubePiece""" 213 result = { 214 (xi, yi, zi): piece 215 for xi, x in enumerate(self._cube) 216 for xi, x in enumerate(self._cube) 217 for yi, y in enumerate(x) 218 for zi, piece in enumerate(y) 219 if xi == 0 or xi == self.size-1 220 or yi == 0 or yi == self.size-1 221 or zi == 0 or zi == self.size-1 # dont include center pieces 222 } 223 return result 224 225 def _move_to_slice(self, move: CubeMove) -> slice: 226 """return the slices affected by a given CubeMove""" 227 228 if not (move.layer >= 1 and move.layer <= self.size): 229 raise CubeException("invalid layer " + str(move.layer)) 230 231 if move.type in (CubeMoveType.R, CubeMoveType.U, CubeMoveType.F): 232 if move.wide: 233 return slice(self.size - move.layer, self.size) 234 235 return slice(self.size - move.layer, self.size - move.layer+1) 236 237 if move.type in (CubeMoveType.L, CubeMoveType.D, CubeMoveType.B): 238 if move.wide: 239 return slice(0, move.layer) 240 241 return slice(move.layer-1, move.layer) 242 243 if move.type in (CubeMoveType.M, CubeMoveType.E, CubeMoveType.S): 244 if self.size % 2 != 1: 245 raise CubeException( 246 "M,E,S moves not allowed for even size cubes") 247 248 return slice(self.size//2, self.size//2+1) 249 250 # move.type in (CubeMoveType.X, CubeMoveType.Y, CubeMoveType.Z): 251 return slice(0, self.size) 252 253 def _get_direction(self, move: CubeMove) -> int: 254 """get the rotation direction for a give CubeMove""" 255 if move.type in (CubeMoveType.R, CubeMoveType.D, CubeMoveType.F, CubeMoveType.E, CubeMoveType.S, CubeMoveType.X, CubeMoveType.Z): 256 direction = -1 257 elif move.type in (CubeMoveType.L, CubeMoveType.U, CubeMoveType.B, CubeMoveType.M, CubeMoveType.Y): 258 direction = 1 259 else: 260 raise CubeException("invalid move face " + str(move.type)) 261 262 if move.is_reversed: 263 direction = direction*-1 264 return direction 265 266 def _rotate_once(self, move: CubeMove) -> None: 267 """Make one cube movement""" 268 if self._store_history: 269 self._history.append(move) 270 271 axis = move.type.get_axis() 272 slices = self._move_to_slice(move) 273 direction = self._get_direction(move) 274 count = move.count 275 276 for _ in range(count): 277 rotation_plane = tuple( 278 slice(None) if i != axis else slices for i in range(3)) 279 rotation_axes = tuple(i for i in range(3) if i != axis) 280 281 plane = self._cube[rotation_plane] 282 rotated_plane = np.rot90(plane, direction, axes=( 283 rotation_axes[0], rotation_axes[1])) 284 self._cube[rotation_plane] = rotated_plane 285 for piece in self._cube[rotation_plane].flatten(): 286 if piece is not None: 287 piece.rotate_piece(axis) 288 289 def rotate(self, movements: Union[str, List[CubeMove]]) -> None: 290 """Make multiple cube movements""" 291 if isinstance(movements, str): 292 movements_list = [CubeMove.create( 293 move_str) for move_str in movements.split(" ") if move_str != ""] 294 else: 295 movements_list = movements 296 297 for move in movements_list: 298 self._rotate_once(move) 299 300 def is_done(self) -> bool: 301 """Returns True if the Cube is done""" 302 for face_name in Face: 303 face = self.get_face_flat(face_name) 304 if any(x != face[0] for x in face): 305 return False 306 return True 307 308 def check_consistency(self) -> bool: 309 """Check the cube for internal consistency""" 310 for face_name in Face: 311 face = self.get_face_flat(face_name) 312 if any((x is None for x in face)): 313 raise CubeException( 314 "cube is not consistent on face " + str(face_name)) 315 return True 316 317 def history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 318 """Return the movement history of the cube""" 319 if to_str: 320 return " ".join([str(x) for x in self._history]) 321 322 return self._history 323 324 def reverse_history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 325 """Return the list of moves to revert the cube history""" 326 reverse = [x.reverse() for x in reversed(self._history)] 327 if to_str: 328 return " ".join([str(x) for x in reverse]) 329 330 return reverse 331 332 def get_kociemba_facelet_colors(self) -> str: 333 """Return the string representation of the cube facelet colors in Kociemba order. 334 The order is: U, R, F, D, L, B. 335 336 Ex: WWWWWWWWWRRRRRRRRRGGGGGGGGGYYYYYYYYYOOOOOOOOOBBBBBBBBB.""" 337 return self.get(face_order=[Face.U, Face.R, Face.F, Face.D, Face.L, Face.B]) 338 339 def get_kociemba_facelet_positions(self) -> str: 340 """Return the string representation of the cube facelet positions in Kociemba order. 341 The order is: U, R, F, D, L, B. 342 343 Ex: UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB.""" 344 facelets = self.get_kociemba_facelet_colors() 345 346 for color, face in ( 347 ('W', 'U'), ('Y', 'D'), 348 ('G', 'F'), ('O', 'L'), 349 ): 350 facelets = facelets.replace(color, face) 351 352 return facelets 353 354 def undo(self, num_moves: int = 1) -> None: 355 """Undo the last num_moves""" 356 if not self._store_history: 357 raise CubeException("can't undo on a cube without history enabled") 358 359 if num_moves > len(self._history): 360 raise CubeException("not enough history to undo") 361 362 reverse_moves = self.reverse_history()[:num_moves] 363 self.rotate(reverse_moves) 364 365 for _ in range(2*num_moves): 366 self._history.pop() 367 368 def __repr__(self): 369 return str(self._cube) 370 371 def __str__(self): 372 printer = CubePrintStr(self) 373 return printer.print_cube()
12class Cube: 13 """Rubik Cube implementation""" 14 15 __slots__ = ("size", "_store_history", "_cube_face_indexes", "_cube_piece_indexes", 16 "_cube_piece_indexes_inv", "_cube", "_history") 17 18 def __init__(self, size: int = 3, state: Optional[str] = None, hist: Optional[bool] = True): 19 20 if size <= 1: 21 raise CubeException("Cube size must be >= 2") 22 23 self.size = size 24 """Cube size""" 25 26 self._store_history = hist 27 28 # record the indexes of every cube face 29 self._cube_face_indexes = [ 30 [[(0, y, z) for z in range(self.size)] 31 for y in reversed(range(self.size))], # L 32 [[(self.size-1, y, z) for z in reversed(range(self.size))] 33 for y in reversed(range(self.size))], # R 34 [[(x, 0, z) for x in range(self.size)] 35 for z in reversed(range(self.size))], # D 36 [[(x, self.size-1, z) for x in range(self.size)] 37 for z in range(self.size)], # U 38 [[(x, y, 0) for x in reversed(range(self.size))] 39 for y in reversed(range(self.size))], # B 40 [[(x, y, self.size-1) for x in range(self.size)] 41 for y in reversed(range(self.size))], # F 42 ] 43 44 # record the indexes of every cube piece 45 self._cube_piece_indexes = [ 46 (x, y, z) 47 for z in range(self.size) 48 for y in range(self.size) 49 for x in range(self.size) 50 if self._is_outer_position(x, y, z) 51 ] 52 self._cube_piece_indexes_inv = { 53 v: idx for idx, v in enumerate(self._cube_piece_indexes)} 54 55 self.reset() 56 if state is not None: 57 self.set(state) 58 59 def _is_outer_position(self, _x: int, _y: int, _z: int) -> bool: 60 """Test if the coordinates indicate and outer cube position""" 61 return _x == 0 or _x == self.size-1 \ 62 or _y == 0 or _y == self.size-1 \ 63 or _z == 0 or _z == self.size-1 # dont include center pieces 64 65 def reset(self): 66 """Reset the cube to the initial configuration""" 67 initial_cube = [ 68 [[CubePiece(self.size, (x, y, z)) 69 if self._is_outer_position(x, y, z) else None 70 for x in range(self.size)] 71 for y in range(self.size)] 72 for z in range(self.size) 73 ] 74 self._cube = np.array(initial_cube, dtype=np.object_) 75 self._history = [] 76 77 def set(self, image: str): 78 """Sets the cube state. 79 80 Parameters 81 ---------- 82 image: str 83 Colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 84 Spaces and newlines are ignored. 85 86 Example: 87 YYYYYYYYY RRRRRRRRR GGGGGGGGG OOOOOOOOO BBBBBBBBB WWWWWWWWW 88 """ 89 image = image.replace(" ", "") 90 image = image.replace("\n", "") 91 92 if len(image) != 6*self.size*self.size: 93 raise CubeException( 94 "Cube state has an invalid size. Should be: " + str(6*self.size*self.size)) 95 96 img = [Color.create(x) for x in image] 97 98 self.reset() 99 for i, color in enumerate(img): 100 face = i // (self.size**2) 101 remain = i % (self.size**2) 102 if face == 0: # U 103 _x = remain % self.size 104 _y = self.size-1 105 _z = remain//self.size 106 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 107 elif face == 5: # D 108 _x = remain % self.size 109 _y = 0 110 _z = self.size-(remain//self.size)-1 111 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 112 elif face == 1: # L 113 _x = 0 114 _y = self.size-(remain//self.size)-1 115 _z = remain % self.size 116 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 117 elif face == 3: # R 118 _x = self.size-1 119 _y = self.size-(remain//self.size)-1 120 _z = self.size-(remain % self.size)-1 121 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 122 elif face == 4: # B 123 _x = self.size-(remain % self.size)-1 124 _y = self.size-(remain//self.size)-1 125 _z = 0 126 self.get_piece((_x, _y, _z)).set_piece_color(2, color) 127 elif face == 2: # F 128 _x = remain % self.size 129 _y = self.size-(remain//self.size)-1 130 _z = self.size-1 131 self.get_piece((_x, _y, _z)).set_piece_color(2, color) 132 133 def get(self, face_order: Optional[List[Face]] = None): 134 """ 135 Get the cube state as a string with the colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 136 137 Example: YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW 138 """ 139 140 if face_order is None: 141 face_order = [Face.U, Face.L, Face.F, Face.R, Face.B, Face.D] 142 143 res = [] 144 for face in face_order: 145 res += self.get_face_flat(face) 146 return "".join([x.name for x in res]) 147 148 def scramble(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 149 """Scramble the cube with random moves. 150 By default scramble only uses wide moves to cubes with size >=4.""" 151 152 movements = self.generate_random_moves(num_steps=num_steps, wide=wide) 153 self.rotate(movements) 154 return movements 155 156 def generate_random_moves(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 157 """Generate a list of random moves (but don't apply them). 158 By default scramble only uses wide moves to cubes with size >=4.""" 159 160 if wide is None and self.size <= 3: 161 wide = False 162 elif wide is None and self.size > 3: 163 wide = True 164 165 possible_moves = [ 166 CubeMoveType.L, CubeMoveType.R, # CubeMoveType.M, 167 CubeMoveType.D, CubeMoveType.U, # CubeMoveType.E, 168 CubeMoveType.B, CubeMoveType.F, # CubeMoveType.S, 169 ] 170 movements = [CubeMove( 171 random.choice(possible_moves), 172 random.choice([False, True]), # reversed 173 random.choice([False, True]) if wide else False, # wide 174 random.randint(1, self.size//2) if wide else 1 # layer 175 ) 176 for _ in range(num_steps)] 177 178 return movements 179 180 def find_piece(self, colors: str) -> Tuple[Coordinates, CubePiece]: 181 """Find the piece with given colors""" 182 colors = "".join(sorted(colors)) 183 for coord, piece in self.get_all_pieces().items(): 184 if colors == piece.get_piece_colors_str(no_loc=True): 185 return coord, piece 186 raise CubeException("piece not found " + colors) 187 188 def get_face(self, face: Face) -> List[List[Color]]: 189 """Get face colors in a multi-dim array""" 190 face_indexes = self._cube_face_indexes[face.value] 191 res = [] 192 for line in face_indexes: 193 line_color = [self._cube[index].get_piece_color( 194 face.get_axis()) for index in line] 195 res.append(line_color) 196 return res 197 198 def get_face_flat(self, face: Face) -> List[Color]: 199 """Get face colors in a flat array""" 200 res = self.get_face(face) 201 return list(np.array(res).flatten()) 202 203 def get_all_faces(self) -> Dict[Face, List[List[Color]]]: 204 """Get the CubePiece of all cube faces""" 205 faces = {f: self.get_face(f) for f in Face} 206 return faces 207 208 def get_piece(self, coordinates: Coordinates) -> CubePiece: 209 """Get the CubePiece at a given coordinate""" 210 return self._cube[coordinates] 211 212 def get_all_pieces(self) -> Dict[Coordinates, CubePiece]: 213 """Return a dictionary of coordinates:CubePiece""" 214 result = { 215 (xi, yi, zi): piece 216 for xi, x in enumerate(self._cube) 217 for xi, x in enumerate(self._cube) 218 for yi, y in enumerate(x) 219 for zi, piece in enumerate(y) 220 if xi == 0 or xi == self.size-1 221 or yi == 0 or yi == self.size-1 222 or zi == 0 or zi == self.size-1 # dont include center pieces 223 } 224 return result 225 226 def _move_to_slice(self, move: CubeMove) -> slice: 227 """return the slices affected by a given CubeMove""" 228 229 if not (move.layer >= 1 and move.layer <= self.size): 230 raise CubeException("invalid layer " + str(move.layer)) 231 232 if move.type in (CubeMoveType.R, CubeMoveType.U, CubeMoveType.F): 233 if move.wide: 234 return slice(self.size - move.layer, self.size) 235 236 return slice(self.size - move.layer, self.size - move.layer+1) 237 238 if move.type in (CubeMoveType.L, CubeMoveType.D, CubeMoveType.B): 239 if move.wide: 240 return slice(0, move.layer) 241 242 return slice(move.layer-1, move.layer) 243 244 if move.type in (CubeMoveType.M, CubeMoveType.E, CubeMoveType.S): 245 if self.size % 2 != 1: 246 raise CubeException( 247 "M,E,S moves not allowed for even size cubes") 248 249 return slice(self.size//2, self.size//2+1) 250 251 # move.type in (CubeMoveType.X, CubeMoveType.Y, CubeMoveType.Z): 252 return slice(0, self.size) 253 254 def _get_direction(self, move: CubeMove) -> int: 255 """get the rotation direction for a give CubeMove""" 256 if move.type in (CubeMoveType.R, CubeMoveType.D, CubeMoveType.F, CubeMoveType.E, CubeMoveType.S, CubeMoveType.X, CubeMoveType.Z): 257 direction = -1 258 elif move.type in (CubeMoveType.L, CubeMoveType.U, CubeMoveType.B, CubeMoveType.M, CubeMoveType.Y): 259 direction = 1 260 else: 261 raise CubeException("invalid move face " + str(move.type)) 262 263 if move.is_reversed: 264 direction = direction*-1 265 return direction 266 267 def _rotate_once(self, move: CubeMove) -> None: 268 """Make one cube movement""" 269 if self._store_history: 270 self._history.append(move) 271 272 axis = move.type.get_axis() 273 slices = self._move_to_slice(move) 274 direction = self._get_direction(move) 275 count = move.count 276 277 for _ in range(count): 278 rotation_plane = tuple( 279 slice(None) if i != axis else slices for i in range(3)) 280 rotation_axes = tuple(i for i in range(3) if i != axis) 281 282 plane = self._cube[rotation_plane] 283 rotated_plane = np.rot90(plane, direction, axes=( 284 rotation_axes[0], rotation_axes[1])) 285 self._cube[rotation_plane] = rotated_plane 286 for piece in self._cube[rotation_plane].flatten(): 287 if piece is not None: 288 piece.rotate_piece(axis) 289 290 def rotate(self, movements: Union[str, List[CubeMove]]) -> None: 291 """Make multiple cube movements""" 292 if isinstance(movements, str): 293 movements_list = [CubeMove.create( 294 move_str) for move_str in movements.split(" ") if move_str != ""] 295 else: 296 movements_list = movements 297 298 for move in movements_list: 299 self._rotate_once(move) 300 301 def is_done(self) -> bool: 302 """Returns True if the Cube is done""" 303 for face_name in Face: 304 face = self.get_face_flat(face_name) 305 if any(x != face[0] for x in face): 306 return False 307 return True 308 309 def check_consistency(self) -> bool: 310 """Check the cube for internal consistency""" 311 for face_name in Face: 312 face = self.get_face_flat(face_name) 313 if any((x is None for x in face)): 314 raise CubeException( 315 "cube is not consistent on face " + str(face_name)) 316 return True 317 318 def history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 319 """Return the movement history of the cube""" 320 if to_str: 321 return " ".join([str(x) for x in self._history]) 322 323 return self._history 324 325 def reverse_history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 326 """Return the list of moves to revert the cube history""" 327 reverse = [x.reverse() for x in reversed(self._history)] 328 if to_str: 329 return " ".join([str(x) for x in reverse]) 330 331 return reverse 332 333 def get_kociemba_facelet_colors(self) -> str: 334 """Return the string representation of the cube facelet colors in Kociemba order. 335 The order is: U, R, F, D, L, B. 336 337 Ex: WWWWWWWWWRRRRRRRRRGGGGGGGGGYYYYYYYYYOOOOOOOOOBBBBBBBBB.""" 338 return self.get(face_order=[Face.U, Face.R, Face.F, Face.D, Face.L, Face.B]) 339 340 def get_kociemba_facelet_positions(self) -> str: 341 """Return the string representation of the cube facelet positions in Kociemba order. 342 The order is: U, R, F, D, L, B. 343 344 Ex: UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB.""" 345 facelets = self.get_kociemba_facelet_colors() 346 347 for color, face in ( 348 ('W', 'U'), ('Y', 'D'), 349 ('G', 'F'), ('O', 'L'), 350 ): 351 facelets = facelets.replace(color, face) 352 353 return facelets 354 355 def undo(self, num_moves: int = 1) -> None: 356 """Undo the last num_moves""" 357 if not self._store_history: 358 raise CubeException("can't undo on a cube without history enabled") 359 360 if num_moves > len(self._history): 361 raise CubeException("not enough history to undo") 362 363 reverse_moves = self.reverse_history()[:num_moves] 364 self.rotate(reverse_moves) 365 366 for _ in range(2*num_moves): 367 self._history.pop() 368 369 def __repr__(self): 370 return str(self._cube) 371 372 def __str__(self): 373 printer = CubePrintStr(self) 374 return printer.print_cube()
Rubik Cube implementation
18 def __init__(self, size: int = 3, state: Optional[str] = None, hist: Optional[bool] = True): 19 20 if size <= 1: 21 raise CubeException("Cube size must be >= 2") 22 23 self.size = size 24 """Cube size""" 25 26 self._store_history = hist 27 28 # record the indexes of every cube face 29 self._cube_face_indexes = [ 30 [[(0, y, z) for z in range(self.size)] 31 for y in reversed(range(self.size))], # L 32 [[(self.size-1, y, z) for z in reversed(range(self.size))] 33 for y in reversed(range(self.size))], # R 34 [[(x, 0, z) for x in range(self.size)] 35 for z in reversed(range(self.size))], # D 36 [[(x, self.size-1, z) for x in range(self.size)] 37 for z in range(self.size)], # U 38 [[(x, y, 0) for x in reversed(range(self.size))] 39 for y in reversed(range(self.size))], # B 40 [[(x, y, self.size-1) for x in range(self.size)] 41 for y in reversed(range(self.size))], # F 42 ] 43 44 # record the indexes of every cube piece 45 self._cube_piece_indexes = [ 46 (x, y, z) 47 for z in range(self.size) 48 for y in range(self.size) 49 for x in range(self.size) 50 if self._is_outer_position(x, y, z) 51 ] 52 self._cube_piece_indexes_inv = { 53 v: idx for idx, v in enumerate(self._cube_piece_indexes)} 54 55 self.reset() 56 if state is not None: 57 self.set(state)
65 def reset(self): 66 """Reset the cube to the initial configuration""" 67 initial_cube = [ 68 [[CubePiece(self.size, (x, y, z)) 69 if self._is_outer_position(x, y, z) else None 70 for x in range(self.size)] 71 for y in range(self.size)] 72 for z in range(self.size) 73 ] 74 self._cube = np.array(initial_cube, dtype=np.object_) 75 self._history = []
Reset the cube to the initial configuration
77 def set(self, image: str): 78 """Sets the cube state. 79 80 Parameters 81 ---------- 82 image: str 83 Colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 84 Spaces and newlines are ignored. 85 86 Example: 87 YYYYYYYYY RRRRRRRRR GGGGGGGGG OOOOOOOOO BBBBBBBBB WWWWWWWWW 88 """ 89 image = image.replace(" ", "") 90 image = image.replace("\n", "") 91 92 if len(image) != 6*self.size*self.size: 93 raise CubeException( 94 "Cube state has an invalid size. Should be: " + str(6*self.size*self.size)) 95 96 img = [Color.create(x) for x in image] 97 98 self.reset() 99 for i, color in enumerate(img): 100 face = i // (self.size**2) 101 remain = i % (self.size**2) 102 if face == 0: # U 103 _x = remain % self.size 104 _y = self.size-1 105 _z = remain//self.size 106 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 107 elif face == 5: # D 108 _x = remain % self.size 109 _y = 0 110 _z = self.size-(remain//self.size)-1 111 self.get_piece((_x, _y, _z)).set_piece_color(1, color) 112 elif face == 1: # L 113 _x = 0 114 _y = self.size-(remain//self.size)-1 115 _z = remain % self.size 116 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 117 elif face == 3: # R 118 _x = self.size-1 119 _y = self.size-(remain//self.size)-1 120 _z = self.size-(remain % self.size)-1 121 self.get_piece((_x, _y, _z)).set_piece_color(0, color) 122 elif face == 4: # B 123 _x = self.size-(remain % self.size)-1 124 _y = self.size-(remain//self.size)-1 125 _z = 0 126 self.get_piece((_x, _y, _z)).set_piece_color(2, color) 127 elif face == 2: # F 128 _x = remain % self.size 129 _y = self.size-(remain//self.size)-1 130 _z = self.size-1 131 self.get_piece((_x, _y, _z)).set_piece_color(2, color)
Sets the cube state.
Parameters
image: str Colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. Spaces and newlines are ignored.
Example: YYYYYYYYY RRRRRRRRR GGGGGGGGG OOOOOOOOO BBBBBBBBB WWWWWWWWW
133 def get(self, face_order: Optional[List[Face]] = None): 134 """ 135 Get the cube state as a string with the colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. 136 137 Example: YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW 138 """ 139 140 if face_order is None: 141 face_order = [Face.U, Face.L, Face.F, Face.R, Face.B, Face.D] 142 143 res = [] 144 for face in face_order: 145 res += self.get_face_flat(face) 146 return "".join([x.name for x in res])
Get the cube state as a string with the colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN.
Example: YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW
148 def scramble(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 149 """Scramble the cube with random moves. 150 By default scramble only uses wide moves to cubes with size >=4.""" 151 152 movements = self.generate_random_moves(num_steps=num_steps, wide=wide) 153 self.rotate(movements) 154 return movements
Scramble the cube with random moves. By default scramble only uses wide moves to cubes with size >=4.
156 def generate_random_moves(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: 157 """Generate a list of random moves (but don't apply them). 158 By default scramble only uses wide moves to cubes with size >=4.""" 159 160 if wide is None and self.size <= 3: 161 wide = False 162 elif wide is None and self.size > 3: 163 wide = True 164 165 possible_moves = [ 166 CubeMoveType.L, CubeMoveType.R, # CubeMoveType.M, 167 CubeMoveType.D, CubeMoveType.U, # CubeMoveType.E, 168 CubeMoveType.B, CubeMoveType.F, # CubeMoveType.S, 169 ] 170 movements = [CubeMove( 171 random.choice(possible_moves), 172 random.choice([False, True]), # reversed 173 random.choice([False, True]) if wide else False, # wide 174 random.randint(1, self.size//2) if wide else 1 # layer 175 ) 176 for _ in range(num_steps)] 177 178 return movements
Generate a list of random moves (but don't apply them). By default scramble only uses wide moves to cubes with size >=4.
180 def find_piece(self, colors: str) -> Tuple[Coordinates, CubePiece]: 181 """Find the piece with given colors""" 182 colors = "".join(sorted(colors)) 183 for coord, piece in self.get_all_pieces().items(): 184 if colors == piece.get_piece_colors_str(no_loc=True): 185 return coord, piece 186 raise CubeException("piece not found " + colors)
Find the piece with given colors
188 def get_face(self, face: Face) -> List[List[Color]]: 189 """Get face colors in a multi-dim array""" 190 face_indexes = self._cube_face_indexes[face.value] 191 res = [] 192 for line in face_indexes: 193 line_color = [self._cube[index].get_piece_color( 194 face.get_axis()) for index in line] 195 res.append(line_color) 196 return res
Get face colors in a multi-dim array
198 def get_face_flat(self, face: Face) -> List[Color]: 199 """Get face colors in a flat array""" 200 res = self.get_face(face) 201 return list(np.array(res).flatten())
Get face colors in a flat array
203 def get_all_faces(self) -> Dict[Face, List[List[Color]]]: 204 """Get the CubePiece of all cube faces""" 205 faces = {f: self.get_face(f) for f in Face} 206 return faces
Get the CubePiece of all cube faces
208 def get_piece(self, coordinates: Coordinates) -> CubePiece: 209 """Get the CubePiece at a given coordinate""" 210 return self._cube[coordinates]
Get the CubePiece at a given coordinate
212 def get_all_pieces(self) -> Dict[Coordinates, CubePiece]: 213 """Return a dictionary of coordinates:CubePiece""" 214 result = { 215 (xi, yi, zi): piece 216 for xi, x in enumerate(self._cube) 217 for xi, x in enumerate(self._cube) 218 for yi, y in enumerate(x) 219 for zi, piece in enumerate(y) 220 if xi == 0 or xi == self.size-1 221 or yi == 0 or yi == self.size-1 222 or zi == 0 or zi == self.size-1 # dont include center pieces 223 } 224 return result
Return a dictionary of coordinates:CubePiece
290 def rotate(self, movements: Union[str, List[CubeMove]]) -> None: 291 """Make multiple cube movements""" 292 if isinstance(movements, str): 293 movements_list = [CubeMove.create( 294 move_str) for move_str in movements.split(" ") if move_str != ""] 295 else: 296 movements_list = movements 297 298 for move in movements_list: 299 self._rotate_once(move)
Make multiple cube movements
301 def is_done(self) -> bool: 302 """Returns True if the Cube is done""" 303 for face_name in Face: 304 face = self.get_face_flat(face_name) 305 if any(x != face[0] for x in face): 306 return False 307 return True
Returns True if the Cube is done
309 def check_consistency(self) -> bool: 310 """Check the cube for internal consistency""" 311 for face_name in Face: 312 face = self.get_face_flat(face_name) 313 if any((x is None for x in face)): 314 raise CubeException( 315 "cube is not consistent on face " + str(face_name)) 316 return True
Check the cube for internal consistency
318 def history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 319 """Return the movement history of the cube""" 320 if to_str: 321 return " ".join([str(x) for x in self._history]) 322 323 return self._history
Return the movement history of the cube
325 def reverse_history(self, to_str: bool = False) -> Union[str, List[CubeMove]]: 326 """Return the list of moves to revert the cube history""" 327 reverse = [x.reverse() for x in reversed(self._history)] 328 if to_str: 329 return " ".join([str(x) for x in reverse]) 330 331 return reverse
Return the list of moves to revert the cube history
333 def get_kociemba_facelet_colors(self) -> str: 334 """Return the string representation of the cube facelet colors in Kociemba order. 335 The order is: U, R, F, D, L, B. 336 337 Ex: WWWWWWWWWRRRRRRRRRGGGGGGGGGYYYYYYYYYOOOOOOOOOBBBBBBBBB.""" 338 return self.get(face_order=[Face.U, Face.R, Face.F, Face.D, Face.L, Face.B])
Return the string representation of the cube facelet colors in Kociemba order. The order is: U, R, F, D, L, B.
Ex: WWWWWWWWWRRRRRRRRRGGGGGGGGGYYYYYYYYYOOOOOOOOOBBBBBBBBB.
340 def get_kociemba_facelet_positions(self) -> str: 341 """Return the string representation of the cube facelet positions in Kociemba order. 342 The order is: U, R, F, D, L, B. 343 344 Ex: UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB.""" 345 facelets = self.get_kociemba_facelet_colors() 346 347 for color, face in ( 348 ('W', 'U'), ('Y', 'D'), 349 ('G', 'F'), ('O', 'L'), 350 ): 351 facelets = facelets.replace(color, face) 352 353 return facelets
Return the string representation of the cube facelet positions in Kociemba order. The order is: U, R, F, D, L, B.
Ex: UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB.
355 def undo(self, num_moves: int = 1) -> None: 356 """Undo the last num_moves""" 357 if not self._store_history: 358 raise CubeException("can't undo on a cube without history enabled") 359 360 if num_moves > len(self._history): 361 raise CubeException("not enough history to undo") 362 363 reverse_moves = self.reverse_history()[:num_moves] 364 self.rotate(reverse_moves) 365 366 for _ in range(2*num_moves): 367 self._history.pop()
Undo the last num_moves