diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index bdfd0ad..5535e8a 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,10 @@ print("State:", cube.get()) print("State (Kociemba):", cube.get_kociemba_facelet_colors()) ``` -## Examples +## Documentation and Examples -See [examples](https://github.com/trincaog/magiccube/tree/main/examples) folder. +- [Code Samples](https://github.com/trincaog/magiccube/tree/main/examples) +- [API Documentation](https://trincaog.github.io/magiccube/magiccube/cube.html) ## Supported Moves and Notation diff --git a/magiccube/__init__.py b/magiccube/__init__.py index 6a7c5a0..13e07d1 100644 --- a/magiccube/__init__.py +++ b/magiccube/__init__.py @@ -1,7 +1,11 @@ """MagicCube: A fast implementation of the Rubik Cube based in Python 3.x.""" +__all__ = ["cube", "cube_base", "cube_piece", + "cube_move", "solver"] + from .cube import Cube from .cube_base import Color, CubeException, Face, PieceType from .cube_piece import CubePiece from .cube_move import CubeMove, CubeMoveType from .cube_print import CubePrintStr, Terminal +from .solver.basic.basic_solver import BasicSolver diff --git a/magiccube/cube.py b/magiccube/cube.py index e062c3a..b36e7d4 100644 --- a/magiccube/cube.py +++ b/magiccube/cube.py @@ -1,5 +1,5 @@ """Rubik Cube implementation""" -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import random import numpy as np from magiccube.cube_base import Color, CubeException, Face @@ -12,14 +12,16 @@ class Cube: """Rubik Cube implementation""" __slots__ = ("size", "_store_history", "_cube_face_indexes", "_cube_piece_indexes", - "_cube_piece_indexes_inv", "cube", "_history") + "_cube_piece_indexes_inv", "_cube", "_history") - def __init__(self, size: int = 3, state=None, hist=True): + def __init__(self, size: int = 3, state: Optional[str] = None, hist: Optional[bool] = True): if size <= 1: raise CubeException("Cube size must be >= 2") self.size = size + """Cube size""" + self._store_history = hist # record the indexes of every cube face @@ -68,7 +70,7 @@ def reset(self): for y in range(self.size)] for z in range(self.size) ] - self.cube = np.array(initial_cube, dtype=np.object_) + self._cube = np.array(initial_cube, dtype=np.object_) self._history = [] def set(self, image: str): @@ -127,7 +129,7 @@ def set(self, image: str): _z = self.size-1 self.get_piece((_x, _y, _z)).set_piece_color(2, color) - def get(self, face_order=None): + def get(self, face_order: Optional[List[Face]] = None): """ Get the cube state as a string with the colors of every cube face in the following order: UP, LEFT, FRONT, RIGHT, BACK, DOWN. @@ -142,7 +144,7 @@ def get(self, face_order=None): res += self.get_face_flat(face) return "".join([x.name for x in res]) - def scramble(self, num_steps: int = 50, wide=None) -> List[CubeMove]: + def scramble(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: """Scramble the cube with random moves. By default scramble only uses wide moves to cubes with size >=4.""" @@ -150,7 +152,7 @@ def scramble(self, num_steps: int = 50, wide=None) -> List[CubeMove]: self.rotate(movements) return movements - def generate_random_moves(self, num_steps: int = 50, wide=None) -> List[CubeMove]: + def generate_random_moves(self, num_steps: int = 50, wide: Optional[bool] = None) -> List[CubeMove]: """Generate a list of random moves (but don't apply them). By default scramble only uses wide moves to cubes with size >=4.""" @@ -187,7 +189,7 @@ def get_face(self, face: Face) -> List[List[Color]]: face_indexes = self._cube_face_indexes[face.value] res = [] for line in face_indexes: - line_color = [self.cube[index].get_piece_color( + line_color = [self._cube[index].get_piece_color( face.get_axis()) for index in line] res.append(line_color) return res @@ -205,15 +207,15 @@ def get_all_faces(self) -> Dict[Face, List[List[Color]]]: def get_piece(self, coordinates: Coordinates) -> CubePiece: """Get the CubePiece at a given coordinate""" - return self.cube[coordinates] + return self._cube[coordinates] def get_all_pieces(self) -> Dict[Coordinates, CubePiece]: """Return a dictionary of coordinates:CubePiece""" - res = [self.cube[x] for x in self._cube_piece_indexes] + res = [self._cube[x] for x in self._cube_piece_indexes] res = { (xi, yi, zi): piece - for xi, x in enumerate(self.cube) + for xi, x in enumerate(self._cube) for yi, y in enumerate(x) for zi, piece in enumerate(y) if xi == 0 or xi == self.size-1 @@ -278,10 +280,10 @@ def _rotate_once(self, move: CubeMove) -> None: slice(None) if i != axis else slices for i in range(3)) rotation_axes = tuple(i for i in range(3) if i != axis) - plane = self.cube[rotation_plane] + plane = self._cube[rotation_plane] rotated_plane = np.rot90(plane, direction, axes=rotation_axes) - self.cube[rotation_plane] = rotated_plane - for piece in self.cube[rotation_plane].flatten(): + self._cube[rotation_plane] = rotated_plane + for piece in self._cube[rotation_plane].flatten(): if piece is not None: piece.rotate_piece(axis) @@ -365,7 +367,7 @@ def undo(self, num_moves: int = 1) -> None: self._history.pop() def __repr__(self): - return str(self.cube) + return str(self._cube) def __str__(self): printer = CubePrintStr(self) diff --git a/magiccube/cube_move.py b/magiccube/cube_move.py index d1e8822..6d961da 100644 --- a/magiccube/cube_move.py +++ b/magiccube/cube_move.py @@ -6,6 +6,8 @@ class CubeMoveType(Enum): + """Cube Move Type""" + L = "L" R = "R" D = "D" @@ -61,6 +63,7 @@ def get_axis(self): str(self.value)) # pragma: no cover def is_cube_rotation(self): + """Return True if the movement type is a whole cube rotation on any of the X,Y,Z axis""" return self in (CubeMoveType.X, CubeMoveType.Y, CubeMoveType.Z) @@ -71,16 +74,25 @@ class CubeMove(): __slots__ = ('type', 'is_reversed', 'wide', 'layer', 'count') - regex_pattern = re.compile( + _regex_pattern = re.compile( "^(?:([0-9]*)(([LRDUBF])([w]?)|([XYZMES]))([']?)([0-9]?))$") # pylint: disable=too-many-positional-arguments def __init__(self, move_type: CubeMoveType, is_reversed: bool = False, wide: bool = False, layer: int = 1, count: int = 1): self.type = move_type + """CubeMoveType""" + self.is_reversed = is_reversed + """True if the move is reversed (counter clock wise)""" + self.wide = wide + """True if the move is wide (2+ layers)""" + self.layer = layer + """Layer of the move (1-N)""" + self.count = count + """Number of repetitions of the move""" @staticmethod def _create_move(result, special_move): @@ -120,7 +132,7 @@ def create(move_str: str): """Create a CubeMove from string representation""" # pylint: disable=too-many-return-statements - result = CubeMove.regex_pattern.match(move_str) + result = CubeMove._regex_pattern.match(move_str) if result is None: raise CubeException("invalid movement " + str(move_str)) result = result.groups() diff --git a/magiccube/cube_print.py b/magiccube/cube_print.py index 3208412..7a7074c 100644 --- a/magiccube/cube_print.py +++ b/magiccube/cube_print.py @@ -14,8 +14,11 @@ class Terminal(Enum): + """Type of terminal for displaying the cube""" default = 0 + """default terminal - no colors""" x256 = 1 + """xterm-256color - colors supported""" class CubePrintStr: @@ -30,7 +33,7 @@ class CubePrintStr: } def __init__(self, cube, terminal: Union[Terminal, None] = None): - self.cube = cube + self._cube = cube if terminal is not None: self.term = terminal else: @@ -64,7 +67,7 @@ def _print_top_down_face(self, cube, face): def print_cube(self): "Print the cube to stdout" - cube = self.cube + cube = self._cube # flatten middle layer print_order_mid = zip(cube.get_face(Face.L), cube.get_face(Face.F), diff --git a/magiccube/solver/basic/__init__.py b/magiccube/solver/basic/__init__.py new file mode 100644 index 0000000..10af0a9 --- /dev/null +++ b/magiccube/solver/basic/__init__.py @@ -0,0 +1 @@ +__all__ = ["basic_solver"] diff --git a/magiccube/solver/basic/basic_solver.py b/magiccube/solver/basic/basic_solver.py index 8c40cd0..b3c53b3 100644 --- a/magiccube/solver/basic/basic_solver.py +++ b/magiccube/solver/basic/basic_solver.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Optional, Tuple from magiccube.cube import Cube, CubeException from magiccube.optimizer.move_optimizer import MoveOptimizer from magiccube.solver.basic.solver_base import SolverException, SolverStage @@ -7,7 +7,7 @@ stage_order_top_corners, stage_turn_top_corners -stages = { +_stages = { "stage_recenter_down": (("W",), stage_recenter_down), "stage_recenter_front": (("G",), stage_recenter_front), @@ -47,23 +47,28 @@ class BasicSolver: + """Cube Solver using the beginner's method""" - def __init__(self, cube: Cube, init_stages=None): + def __init__(self, cube: Cube): if cube.size != 3: raise SolverException("Solver only works with 3x3x3 cube") - self.cube = cube - self.stages: List[SolverStage] = [] - self.default_debug = False - self.max_iterations_per_stage = 12 + self._cube = cube + self._stages: List[SolverStage] = [] + self._default_debug = False + self._max_iterations_per_stage = 12 + self._set_stages() + + def _set_stages(self, init_stages: Optional[List[str]] = None): + """Method used for testing: Set the stages to be used for solving the cube.""" if init_stages is None: - for name, stage in stages.items(): - self.add( - name=name, target_colors=stage[0], pattern_condition_actions=stage[1], debug=self.default_debug) + for name, stage in _stages.items(): + self._add( + name=name, target_colors=stage[0], pattern_condition_actions=stage[1], debug=self._default_debug) else: for init_stage in init_stages: - self.add(name=init_stage, target_colors=stages[init_stage][0], - pattern_condition_actions=stages[init_stage][1], debug=self.default_debug) + self._add(name=init_stage, target_colors=_stages[init_stage][0], + pattern_condition_actions=_stages[init_stage][1], debug=self._default_debug) def _solve_pattern_stage(self, stage: SolverStage) -> List[str]: """Solve one stage of the cube""" @@ -71,31 +76,31 @@ def _solve_pattern_stage(self, stage: SolverStage) -> List[str]: full_actions = [] iteration = 0 - while iteration < self.max_iterations_per_stage: + while iteration < self._max_iterations_per_stage: iteration += 1 - target_pieces = [self.cube.find_piece( + target_pieces = [self._cube.find_piece( target_color) for target_color in stage.target_colors] if stage.debug: # pragma:no cover print("solve_stage start:", stage.name, stage.target_colors, target_pieces) - print(self.cube) + print(self._cube) actions, is_continue = stage.get_moves(target_pieces) - self.cube.rotate(actions) + self._cube.rotate(actions) full_actions += actions if stage.debug: # pragma:no cover print("solve_stage end:", stage.name, target_pieces, actions, is_continue) - print(self.cube) + print(self._cube) if not is_continue: # stage is complete break - if iteration >= self.max_iterations_per_stage: + if iteration >= self._max_iterations_per_stage: raise SolverException(f"stage iteration limit exceeded: {stage}") return full_actions @@ -104,7 +109,7 @@ def solve(self, optimize=True): """Solve the cube by running all the registered pattern stages""" try: full_actions = [] - for stage in self.stages: + for stage in self._stages: if stage.debug: # pragma:no cover print("starting stage", stage) actions = self._solve_pattern_stage(stage) @@ -117,8 +122,8 @@ def solve(self, optimize=True): except CubeException as e: raise SolverException("unable to solve cube", e) from e - def add(self, name, target_colors: Tuple[str, ...], pattern_condition_actions: Tuple[ConditionAction, ...], debug=False): + def _add(self, name, target_colors: Tuple[str, ...], pattern_condition_actions: Tuple[ConditionAction, ...], debug=False): """Add a stage to the solver.""" - self.stages.append(SolverStage( + self._stages.append(SolverStage( target_colors, pattern_condition_actions, name=name, debug=debug)) return self diff --git a/magiccube/solver/basic/solver_base.py b/magiccube/solver/basic/solver_base.py index 82215da..dd0f799 100644 --- a/magiccube/solver/basic/solver_base.py +++ b/magiccube/solver/basic/solver_base.py @@ -6,7 +6,7 @@ class SolverException(Exception): - pass + """Exception raised when the solver fails to find a solution""" @dataclass diff --git a/test/test_cube.py b/test/test_cube.py index 50ffa8e..fce2bd2 100644 --- a/test/test_cube.py +++ b/test/test_cube.py @@ -424,7 +424,9 @@ def test_get_cube(): def test_inconsistent_cube(): c = Cube(3) - c.cube[0, 0, 0] = CubePiece(colors=[None, None, None]) + + # pylint: disable=protected-access + c._cube[0, 0, 0] = CubePiece(colors=[None, None, None]) with pytest.raises(CubeException): c.check_consistency() diff --git a/test/test_solver.py b/test/test_solver.py index b2191bb..bb33a5e 100644 --- a/test/test_solver.py +++ b/test/test_solver.py @@ -33,7 +33,8 @@ def test_solve_nok_max_iterations(): random.seed(42) cube.scramble(num_steps=50, wide=False) solver = BasicSolver(cube) - solver.max_iterations_per_stage = 2 + # pylint: disable=protected-access + solver._max_iterations_per_stage = 2 with pytest.raises(SolverException): solver.solve() @@ -55,7 +56,9 @@ def test_solve_white_cross(): ] cube = Cube(hist=False, size=3) - solver = BasicSolver(cube, init_stages=init_stages) + solver = BasicSolver(cube) + # pylint: disable=protected-access + solver._set_stages(init_stages) random.seed(42) cube.scramble(num_steps=50, wide=False) @@ -92,7 +95,9 @@ def test_solve_white_corners(): ] cube = Cube(hist=False, size=3) - solver = BasicSolver(cube, init_stages=init_stages) + solver = BasicSolver(cube) + # pylint: disable=protected-access + solver._set_stages(init_stages) random.seed(42) cube.scramble(num_steps=50, wide=False) @@ -148,7 +153,9 @@ def test_solve_2nd_layer(): ] cube = Cube(hist=False, size=3) - solver = BasicSolver(cube, init_stages=init_stages) + solver = BasicSolver(cube) + # pylint: disable=protected-access + solver._set_stages(init_stages) random.seed(42) cube.scramble(num_steps=50, wide=False) @@ -223,7 +230,9 @@ def test_solve_top_cross(): ] cube = Cube(hist=False, size=3) - solver = BasicSolver(cube, init_stages=init_stages) + solver = BasicSolver(cube) + # pylint: disable=protected-access + solver._set_stages(init_stages) random.seed(42) cube.scramble(num_steps=50, wide=False) @@ -313,7 +322,9 @@ def test_solve_top_corners(): ] cube = Cube(hist=False, size=3) - solver = BasicSolver(cube, init_stages=init_stages) + solver = BasicSolver(cube) + # pylint: disable=protected-access + solver._set_stages(init_stages) random.seed(42) cube.scramble(num_steps=50, wide=False)