Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions magiccube/__init__.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 17 additions & 15 deletions magiccube/cube.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.

Expand All @@ -142,15 +144,15 @@ 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."""

movements = self.generate_random_moves(num_steps=num_steps, wide=wide)
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."""

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
16 changes: 14 additions & 2 deletions magiccube/cube_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class CubeMoveType(Enum):
"""Cube Move Type"""

L = "L"
R = "R"
D = "D"
Expand Down Expand Up @@ -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)


Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 5 additions & 2 deletions magiccube/cube_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions magiccube/solver/basic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["basic_solver"]
47 changes: 26 additions & 21 deletions magiccube/solver/basic/basic_solver.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),

Expand Down Expand Up @@ -47,55 +47,60 @@


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"""

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
Expand All @@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion magiccube/solver/basic/solver_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class SolverException(Exception):
pass
"""Exception raised when the solver fails to find a solution"""


@dataclass
Expand Down
4 changes: 3 additions & 1 deletion test/test_cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading