From 1ee25b6b0f4481d204cda674c83b191cdb923084 Mon Sep 17 00:00:00 2001 From: trincaog Date: Fri, 22 Jul 2022 00:37:24 +0100 Subject: [PATCH 1/2] Create cube in a specific position Cleanups --- README.md | 2 +- examples/cube3x3.py | 14 ++--- examples/cube4x4.py | 19 +++++++ examples/solve_cube_3x3.py | 5 +- magiccube/cube.py | 75 ++++++++++++++++++++++---- magiccube/cube_piece.py | 17 ++++-- magiccube/solver/basic/basic_solver.py | 38 +++++++------ test/test_cube.py | 61 +++++++++++++++++++++ 8 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 examples/cube4x4.py diff --git a/README.md b/README.md index ff75ae8..bd221aa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ pip install magiccube import magiccube # 3x3x3 Cube -cube = magiccube.Cube(3) +cube = magiccube.Cube(3,"YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW") print(cube) ``` ![Cube](https://trincaopub.s3.amazonaws.com/imgs/magiccube/cube3x3.png) diff --git a/examples/cube3x3.py b/examples/cube3x3.py index 2599290..df8a15b 100644 --- a/examples/cube3x3.py +++ b/examples/cube3x3.py @@ -1,7 +1,10 @@ import magiccube -# Create the cube -cube = magiccube.Cube(3) +# Create the cube in solved state +cube = magiccube.Cube(3,"YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW") + +# Print the cube +print(cube) # Make some cube rotations cube.rotate("R' L U D' F B' R' L") @@ -9,16 +12,15 @@ # Print the cube print(cube) +# Create the cube with a fixed state +cube = magiccube.Cube(3, "YYYYYYGGGGGWRRRRRROOOGGWGGWYBBOOOOOORRRYBBYBBWWBWWBWWB") + # Reset to the initial position cube.reset() # Scramble the cube cube.scramble() -# Print the cube -print("Scrambled cube") -print(cube) - # Print the move history print("History: ", cube.history()) diff --git a/examples/cube4x4.py b/examples/cube4x4.py new file mode 100644 index 0000000..edbb088 --- /dev/null +++ b/examples/cube4x4.py @@ -0,0 +1,19 @@ +import magiccube + +# Create the cube +cube = magiccube.Cube(4,state=""" + YYYYYYYYYYYYGGGG + GGGWRRRRRRRRRRRR + OOOOGGGWGGGWGGGW + YBBBOOOOOOOOOOOO + RRRRYBBBYBBBYBBB + WWWBWWWBWWWBWWWB + """) + +# Print the cube +print(cube) + +# Make some cube rotations +cube.rotate("U' R'") + + diff --git a/examples/solve_cube_3x3.py b/examples/solve_cube_3x3.py index 690c2f5..6123613 100644 --- a/examples/solve_cube_3x3.py +++ b/examples/solve_cube_3x3.py @@ -10,7 +10,7 @@ cube = Cube(hist=False, size=3) solver = BasicSolver(cube) -# Solve the cube 100 times +# Solve the cube N times for _ in range(5): # Reset & Scramble the cube cube.reset() @@ -23,4 +23,5 @@ # Solve the cube actions = solver.solve() print("Solution actions:", actions) - print() + + assert cube.is_done() diff --git a/magiccube/cube.py b/magiccube/cube.py index caf49aa..7c7c246 100644 --- a/magiccube/cube.py +++ b/magiccube/cube.py @@ -7,19 +7,24 @@ from magiccube.cube_move import CubeMove, CubeMoveType from magiccube.cube_print import CubePrintStr +class CubeException(Exception): + pass + class Cube: """Rubik Cube implementation""" __slots__=("size","_store_history","_cube_face_indexes","_cube_piece_indexes", "_cube_piece_indexes_inv","cube","_history") - def __init__(self, size: int=3, hist=True): + def __init__(self, size: int=3, state=None, hist=True): if size<=1: - raise Exception("Cube size must be >= 2") + raise CubeException("Cube size must be >= 2") self.size = size self._store_history = hist + + # record the indexes of every cube face self._cube_face_indexes = [ [[(0,y,z) for z in range(self.size)] for y in reversed(range(self.size))], #L @@ -35,6 +40,7 @@ def __init__(self, size: int=3, hist=True): for y in reversed(range(self.size))], #F ] + # record the indexes of every cube piece self._cube_piece_indexes = [ (x,y,z) for z in range(self.size) @@ -44,9 +50,12 @@ def __init__(self, size: int=3, hist=True): ] self._cube_piece_indexes_inv={v:idx for idx,v in enumerate(self._cube_piece_indexes)} - self.reset() + if state is None: + self.reset() + else: + self.set(state) - def _is_outer_position(self,_z:int,_y:int,_x:int)->bool: + def _is_outer_position(self,_x:int,_y:int,_z:int)->bool: """Test if the coordinates indicate and outer cube position""" return _x==0 or _x==self.size-1 \ or _y==0 or _y==self.size-1 \ @@ -64,6 +73,51 @@ def reset(self): self.cube = np.array(initial_cube, dtype=np.object_) self._history = [] + def set(self, image:str): + """Sets the cube state""" + image = image.replace(" ", "") + image = image.replace("\n", "") + + if len(image) != 6*self.size*self.size: + raise CubeException("Cube state has an invalid size. Should be: " + str(6*self.size*self.size)) + + img = [CubeColor.create(x) for x in image] + + self.reset() + for i,c in enumerate(img): + face = i // (self.size**2) + remain = i%(self.size**2) + if face ==0: #U + x=remain%self.size + y = self.size-1 + z=remain//self.size + self.get_piece((x,y,z)).set_piece_color(1,c) + elif face == 5: #D + x=remain%self.size + y = 0 + z=self.size-(remain//self.size)-1 + self.get_piece((x,y,z)).set_piece_color(1,c) + elif face == 1: #L + x = 0 + y=self.size-(remain//self.size)-1 + z=remain%self.size + self.get_piece((x,y,z)).set_piece_color(0,c) + elif face == 3: #R + x = self.size-1 + y=self.size-(remain//self.size)-1 + z=self.size-(remain%self.size)-1 + self.get_piece((x,y,z)).set_piece_color(0,c) + elif face == 4: #B + x=self.size-(remain%self.size)-1 + y = self.size-(remain//self.size)-1 + z=0 + self.get_piece((x,y,z)).set_piece_color(2,c) + elif face == 2: #F + x=remain%self.size + y=self.size-(remain//self.size)-1 + z=self.size-1 + self.get_piece((x,y,z)).set_piece_color(2,c) + def scramble(self, num_steps:int=50, wide=None) -> List[CubeMove]: """Scramble the cube with random moves. By default scramble only uses wide moves to cubes with size >=4.""" @@ -102,7 +156,7 @@ def find_piece(self, colors:str) -> Tuple[CubeCoordinates, CubePiece]: for coord, piece in self.get_all_pieces().items(): if colors == piece.get_piece_colors_str(no_loc=True): return coord,piece - raise Exception ("piece not found " + colors) + raise CubeException("piece not found " + colors) def get_face(self, face:CubeFace)->List[List[CubeColor]]: """Get face colors in a multi-dim array""" @@ -146,7 +200,8 @@ def get_all_pieces(self)->Dict[CubeCoordinates,CubePiece]: def _move_to_index(self, move:CubeMove): """return the indexes affted by a given CubeMove""" - assert move.layer>=1 and move.layer<=self.size,"invalid layer " + str(move.layer) + if not(move.layer>=1 and move.layer<=self.size): + raise CubeException("invalid layer " + str(move.layer)) if move.type in (CubeMoveType.R, CubeMoveType.U, CubeMoveType.F): if move.wide: @@ -159,7 +214,9 @@ def _move_to_index(self, move:CubeMove): else: return (move.layer-1,) elif move.type in (CubeMoveType.M, CubeMoveType.E, CubeMoveType.S): - assert self.size%2==1, "M,E,S moves not allow for even sizes" + if self.size%2 != 1: + raise CubeException("M,E,S moves not allowed for even size cubes") + return (self.size//2,) else: # move.type in (CubeMoveType.X, CubeMoveType.Y, CubeMoveType.Z): return tuple(range(self.size)) @@ -171,7 +228,7 @@ def _get_direction(self,move:CubeMove)->int: elif move.type in (CubeMoveType.L,CubeMoveType.U,CubeMoveType.B,CubeMoveType.M, CubeMoveType.Y): direction = 1 else: - raise Exception("invalid move face " + str(move.type)) + raise CubeException("invalid move face " + str(move.type)) if move.is_reversed: direction=direction*-1 @@ -220,7 +277,7 @@ def check_consistency(self, raise_exception = True): face = self.get_face(face_name) if any((x is None for x in face)): if raise_exception: - raise Exception("cube is not consistent on face "+ str(face_name)) + raise CubeException("cube is not consistent on face "+ str(face_name)) return False return True diff --git a/magiccube/cube_piece.py b/magiccube/cube_piece.py index 81638e2..bdc57f3 100644 --- a/magiccube/cube_piece.py +++ b/magiccube/cube_piece.py @@ -1,4 +1,5 @@ """Cube Piece implementation""" +from typing import List, Optional import numpy as np from magiccube.cube_base import PieceColor, CubeColor, CubeCoordinates, PieceType @@ -7,11 +8,17 @@ class CubePiece: __slots__ = ('_colors',) - def __init__(self, cube_size: int, position:CubeCoordinates): - #self.initial_position = position - self._colors = self._build_piece_colors(cube_size, position) + def __init__(self, cube_size: Optional[int]=None, position:Optional[CubeCoordinates]=None, + colors:Optional[List[Optional[CubeColor]]]=None): + if cube_size is not None and position is not None: + self._colors = self._build_piece_colors(cube_size, position) + elif colors is not None and len(colors)==3: + self._colors = np.array(colors) + else: + assert False, "Can't create CubePiece. Either position or color must be specified." def _build_piece_colors(self, cube_size:int, position:CubeCoordinates) ->np.ndarray: + """Creates the default piece colors""" (_z,_y,_x)=position if _x == 0: @@ -62,6 +69,10 @@ def get_piece_colors(self, no_loc=False)->PieceColor: return tuple(colors) + def set_piece_color(self, axis:int, color:CubeColor): + """Set the piece colors""" + self._colors[axis]=color + def rotate_piece(self, axis:int) -> None: """Rotate the piece colors according to a given movement""" diff --git a/magiccube/solver/basic/basic_solver.py b/magiccube/solver/basic/basic_solver.py index cdb4d86..de63a24 100644 --- a/magiccube/solver/basic/basic_solver.py +++ b/magiccube/solver/basic/basic_solver.py @@ -1,5 +1,5 @@ from typing import List, Tuple -from magiccube.cube import Cube +from magiccube.cube import Cube, CubeException from magiccube.optimizer.move_optimizer import MoveOptimizer from magiccube.solver.basic.solver_base import SolverStage from magiccube.solver.basic.solver_stages import * @@ -43,11 +43,14 @@ "stage_turn_top_corners": (("YRG","YRB","YBO","YGO"), stage_turn_top_corners), } +class SolverException(Exception): + pass class BasicSolver: def __init__(self, cube:Cube, init_stages=None): - assert cube.size==3, "Solver only works with 3x3x3 cube" + if cube.size!=3: + raise SolverException("Solver only works with 3x3x3 cube") self.cube = cube self.stages:List[SolverStage]=[] self.default_debug =False @@ -88,23 +91,28 @@ def _solve_pattern_stage(self, stage:SolverStage)-> List[str]: # stage is complete break - assert iteration < max_iter, f"stage iteration limit exceeded: {stage}" + if iteration >= max_iter: + raise SolverException(f"stage iteration limit exceeded: {stage}") + return full_actions def solve(self, optimize=True): """Solve the cube by running all the registered pattern stages""" - full_actions=[] - for stage in self.stages: - if stage.debug: - print("starting stage",stage) - actions = self._solve_pattern_stage(stage) - full_actions += actions - - #assert self.cube.is_done() , "cube not done" - if optimize: - full_actions = MoveOptimizer().optimize(full_actions) - - return full_actions + try: + full_actions=[] + for stage in self.stages: + if stage.debug: + print("starting stage",stage) + actions = self._solve_pattern_stage(stage) + full_actions += actions + + #assert self.cube.is_done() , "cube not done" + if optimize: + full_actions = MoveOptimizer().optimize(full_actions) + + return full_actions + except CubeException as e: + raise SolverException("unable to solve cube",e) def add(self,name, target_colors:Tuple[str,...], pattern_condition_actions:Tuple[ConditionAction, ...], debug=False): diff --git a/test/test_cube.py b/test/test_cube.py index d2a2136..ff71f96 100644 --- a/test/test_cube.py +++ b/test/test_cube.py @@ -1,3 +1,4 @@ +from kiwisolver import Solver import numpy as np from magiccube import Cube from magiccube.cube_base import CubeFace, PieceType @@ -6,6 +7,8 @@ import pytest import random +from magiccube.solver.basic.basic_solver import BasicSolver, SolverException + def test_reset(): c = Cube(3) c.check_consistency() @@ -255,6 +258,64 @@ def test_get_piece_type(): inner = c.get_piece(coordinates=(1,1,1)) assert inner is None or inner.get_piece_type()==PieceType.INNER +def test_set_cube(): + c = Cube(3) + c.set("YYYYYYYYYRRRRRRRRRGGGGGGGGGOOOOOOOOOBBBBBBBBBWWWWWWWWW") + assert c.is_done() + +def test_set_cube_4x(): + c = Cube(4) + c.set("YYYYYYYYYYYYYYYYRRRRRRRRRRRRRRRRGGGGGGGGGGGGGGGGOOOOOOOOOOOOOOOOBBBBBBBBBBBBBBBBWWWWWWWWWWWWWWWW") + assert c.is_done() + +def test_set_cube_initial_state(): + c = Cube(3, state="YYYYYYGGGGGWRRRRRROOOGGWGGWYBBOOOOOORRRYBBYBBWWBWWBWWB") + c.rotate("U' R'") + + assert c.is_done() + +def test_set_cube_initial_state_4X(): + c = Cube(4, state=""" + YYYYYYYYYYYYGGGG + GGGWRRRRRRRRRRRR + OOOOGGGWGGGWGGGW + YBBBOOOOOOOOOOOO + RRRRYBBBYBBBYBBB + WWWBWWWBWWWBWWWB + """) + c.rotate("U' R'") + + assert c.is_done() + +def test_set_cube_not_done(): + c = Cube(3) + c.set(""" + RBB BYO GGO + YRO YRW WWW + YYB GGW BRW + YYR OOW GGW + YYG BBR OOG + RGO RWO RBB + """) + c.rotate("D' B' R' F' L' U'") + + assert c.is_done() + +def test_set_cube_bad_cube(): + c = Cube(3) + c.set(""" + RBB BYO OGG + YRO YRW WWW + YYB GGW BRW + YYR OOW GGW + YYG BBR OOG + RGO RWO RBB + """) + solver = BasicSolver(c) + with pytest.raises(SolverException): + solver.solve() + + if __name__ == "__main__" : pytest.main() pass From 4d76d2b340ad6089f673bdcc8198fe91fdacc257 Mon Sep 17 00:00:00 2001 From: trincaog Date: Fri, 22 Jul 2022 00:38:57 +0100 Subject: [PATCH 2/2] updated version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2dd8d63..5f58742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "magiccube" -version = "0.0.5" +version = "0.0.6" authors = [ { name="Gonçalo Trincão Cunha", email="goncalo.cunha@gmail.com" }, ]