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()
class 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

Cube( size: int = 3, state: Optional[str] = None, hist: Optional[bool] = True)
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)
size

Cube size

def reset(self):
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

def set(self, image: str):
 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

def get(self, face_order: Optional[List[magiccube.cube_base.Face]] = None):
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

def scramble( self, num_steps: int = 50, wide: Optional[bool] = None) -> List[magiccube.cube_move.CubeMove]:
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.

def generate_random_moves( self, num_steps: int = 50, wide: Optional[bool] = None) -> List[magiccube.cube_move.CubeMove]:
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.

def find_piece( self, colors: str) -> Tuple[Tuple[int, int, int], magiccube.cube_piece.CubePiece]:
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

def get_face( self, face: magiccube.cube_base.Face) -> List[List[magiccube.cube_base.Color]]:
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

def get_face_flat(self, face: magiccube.cube_base.Face) -> List[magiccube.cube_base.Color]:
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

def get_all_faces( self) -> Dict[magiccube.cube_base.Face, List[List[magiccube.cube_base.Color]]]:
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

def get_piece( self, coordinates: Tuple[int, int, int]) -> magiccube.cube_piece.CubePiece:
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

def get_all_pieces(self) -> Dict[Tuple[int, int, int], magiccube.cube_piece.CubePiece]:
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

def rotate(self, movements: Union[str, List[magiccube.cube_move.CubeMove]]) -> None:
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

def is_done(self) -> bool:
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

def check_consistency(self) -> bool:
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

def history( self, to_str: bool = False) -> Union[str, List[magiccube.cube_move.CubeMove]]:
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

def reverse_history( self, to_str: bool = False) -> Union[str, List[magiccube.cube_move.CubeMove]]:
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

def get_kociemba_facelet_colors(self) -> str:
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.

def get_kociemba_facelet_positions(self) -> str:
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.

def undo(self, num_moves: int = 1) -> None:
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