diff --git a/skewb_solver.py b/skewb_solver.py index c28cac2..1d76e4b 100644 --- a/skewb_solver.py +++ b/skewb_solver.py @@ -13,39 +13,35 @@ import pytest print() # twisting the bottom, opposing this color clockwise when this color is facing away -Axis = Literal["R", "B", "O", "G"] -axes: tuple[Axis, ...] = Axis.__args__ -# how many clockwise twists along an axis has been twisted after being flush with the top or bottom. -CornerRotation = Literal[0, 1, 2] +# RBOG +Axis = Literal["G", "O", "B", "R"] +AXES: tuple[Axis, ...] = Axis.__args__ +# How many clockwise twists along an axis has been twisted after being flush with the top or bottom. +CornerRotState = Literal[0, 1, 2] -LowerAntiAxis = Literal["R", "B", "O", "G", "r", "b", "o", "g"] -lower_anti_axes: tuple[LowerAntiAxis, ...] = LowerAntiAxis.__args__ -to_anticlockwise: dict[Axis, LowerAntiAxis] = { - "R": "r", - "B": "b", - "O": "o", - "G": "g", -} -to_clockwise: dict[LowerAntiAxis, Axis] = {v: k for k, v in to_anticlockwise.items()} -to_opposite: dict[LowerAntiAxis, LowerAntiAxis] = {**to_anticlockwise, **to_clockwise} +Twist = Literal["G", "O", "B", "R", "g", "o", "b", "r"] +TWISTS: tuple[Twist, ...] = Twist.__args__ +to_anticlockwise: dict[Axis, Twist] = {"G": "g", "O": "o", "B": "b", "R": "r"} +to_clockwise: dict[Twist, Axis] = {v: k for k, v in to_anticlockwise.items()} +to_opposite: dict[Twist, Twist] = {**to_anticlockwise, **to_clockwise} @dataclass(frozen=True) class Corner: col: Axis - rot: CornerRotation + rot: CornerRotState def __repr__(self) -> str: return f"{self.col}{self.rot}" -MidRotatation = Literal["Y", "RG", "RB"] +MidRotState = Literal["Y", "O", "R"] @dataclass(frozen=True) class Middle: - col: Literal["R", "B", "O", "G", "Y"] - rot: MidRotatation + col: Literal["G", "O", "B", "R", "Y"] + rot: MidRotState def __repr__(self) -> str: return f"{self.col}{self.rot}" @@ -56,10 +52,8 @@ class Skewb: """Represents a Rubics cube variant sold as 'Qiyi Twisty Skewb'.""" # order: vertically below Axis primary color - # "R", "B", "O", "G" top: Tuple[Corner, Corner, Corner, Corner] bot: Tuple[Corner, Corner, Corner, Corner] - # order: RB BO OG GR Y mids: Tuple[Middle, Middle, Middle, Middle, Middle] # def __post_init__(self): @@ -68,89 +62,74 @@ class Skewb: def assert_valid(self): assert Counter( corner.col for corners in [self.top, self.bot] for corner in corners - ) == {col: 2 for col in axes} + ) == {col: 2 for col in AXES} assert Counter(mid.col for mid in self.mids) == Counter("RBOGY") # forbidden rotations - assert self.mids[0].rot != "RB" - assert self.mids[1].rot != "RG" - assert self.mids[2].rot != "RB" - assert self.mids[3].rot != "RG" + assert self.mids[0].rot != "R" + assert self.mids[1].rot != "O" + assert self.mids[2].rot != "R" + assert self.mids[3].rot != "O" assert self.mids[4].col == "Y" or self.mids[4].rot != "Y" -R0 = Corner("R", 0) -R1 = Corner("R", 1) -R2 = Corner("R", 2) -B0 = Corner("B", 0) -B1 = Corner("B", 1) -B2 = Corner("B", 2) -O0 = Corner("O", 0) -O1 = Corner("O", 1) -O2 = Corner("O", 2) G0 = Corner("G", 0) G1 = Corner("G", 1) G2 = Corner("G", 2) +O0 = Corner("O", 0) +O1 = Corner("O", 1) +O2 = Corner("O", 2) +B0 = Corner("B", 0) +B1 = Corner("B", 1) +B2 = Corner("B", 2) +R0 = Corner("R", 0) +R1 = Corner("R", 1) +R2 = Corner("R", 2) -RY = Middle("R", "Y") -RRG = Middle("R", "RG") -RRB = Middle("R", "RB") -BY = Middle("B", "Y") -BRG = Middle("B", "RG") -BRB = Middle("B", "RB") -OY = Middle("O", "Y") -ORG = Middle("O", "RG") -ORB = Middle("O", "RB") +GR = Middle("G", "R") +OR = Middle("O", "R") +BR = Middle("B", "R") +RR = Middle("R", "R") +GO = Middle("G", "O") +OO = Middle("O", "O") +BO = Middle("B", "O") +RO = Middle("R", "O") GY = Middle("G", "Y") -GRG = Middle("G", "RG") -GRB = Middle("G", "RB") +OY = Middle("O", "Y") +BY = Middle("B", "Y") +RY = Middle("R", "Y") YY = Middle("Y", "Y") -YRG = Middle("Y", "RG") -YRB = Middle("Y", "RB") -def rotate_side_mid_rot_about_W(m: Middle) -> Middle: +def rotate_mid_about_W(m: Middle) -> Middle: return Middle( - col=m.col, - rot="Y" if m.rot == "Y" else ("RB" if m.rot == "RG" else "RG"), + col=m.col, rot="Y" if m.rot == "Y" else ("R" if m.rot == "O" else "O") ) -def rotate_bot_mid_rot_about_W(m: Middle) -> Middle: - if m.col == "Y": - return Middle("Y", "Y") - return Middle(col=m.col, rot="RB" if m.rot == "RG" else "RG") - - def rotate_everything_about_W(s: Skewb) -> Skewb: """Clockwise rotation when looking down upon white.""" return Skewb( top=(s.top[-1],) + s.top[:-1], bot=(s.bot[-1],) + s.bot[:-1], mids=( - rotate_side_mid_rot_about_W(s.mids[3]), - rotate_side_mid_rot_about_W(s.mids[0]), - rotate_side_mid_rot_about_W(s.mids[1]), - rotate_side_mid_rot_about_W(s.mids[2]), - rotate_side_mid_rot_about_W(s.mids[4]), + rotate_mid_about_W(s.mids[3]), + rotate_mid_about_W(s.mids[0]), + rotate_mid_about_W(s.mids[1]), + rotate_mid_about_W(s.mids[2]), + rotate_mid_about_W(s.mids[4]), ), ) -desk_start = Skewb( - top=(R0, B0, O0, G2), - bot=(B0, O0, G1, R0), - mids=(BY, OY, RRG, GRB, YY), -) - solved_skewb = Skewb( - top=(O0, B0, R0, G0), - bot=(B0, R0, G0, O0), - mids=(BY, RY, GY, OY, YY), + top=(G0, O0, B0, R0), + bot=(G0, O0, B0, R0), + mids=(GY, OY, BY, RY, YY), ) -@pytest.mark.parametrize("s", [desk_start, solved_skewb]) +@pytest.mark.parametrize("s", [solved_skewb]) def test_rotate_everything_about_W(s: Skewb): ss = [s] for i in range(4): @@ -160,10 +139,10 @@ def test_rotate_everything_about_W(s: Skewb): def test_axes(): - assert axes[0] == "R" + assert AXES[0] == "G" -type CornerRotPermutation = dict[CornerRotation, CornerRotation] +type CornerRotPermutation = dict[CornerRotState, CornerRotState] BOT_LEFT_TO_TOP: CornerRotPermutation = {0: 2, 1: 0, 2: 1} TOP_TO_BOT_RIGHT: CornerRotPermutation = {0: 2, 1: 0, 2: 1} ROTATE_CORNER_CLOCKWISE: CornerRotPermutation = {0: 1, 1: 2, 2: 0} @@ -179,15 +158,11 @@ def test_rotation_permutations(p: CornerRotPermutation): assert set(p.values()) == {0, 1, 2} -MID_DIR_INCREMENT: dict[MidRotatation, MidRotatation] = { - "RG": "Y", - "Y": "RB", - "RB": "RG", -} +MID_DIR_INCREMENT: dict[MidRotState, MidRotState] = {"Y": "O", "R": "Y", "O": "R"} def clockwise_twist(s: Skewb, twist: Axis) -> Skewb: - rot_before, rot_after = {"R": (0, 0), "B": (3, 1), "O": (2, 2), "G": (1, 3)}[twist] + rot_before, rot_after = {"G": (0, 0), "O": (3, 1), "B": (2, 2), "R": (1, 3)}[twist] for _ in range(rot_before): s = rotate_everything_about_W(s) s = Skewb( @@ -207,20 +182,16 @@ def clockwise_twist(s: Skewb, twist: Axis) -> Skewb: s.mids[0], Middle( (m := s.mids[2]).col, - "Y" - if m.col == "Y" - else MID_DIR_INCREMENT[m.rot], # ("Y" if m.rot == "RG" else "RB"), + "Y" if m.col == "Y" else MID_DIR_INCREMENT[m.rot], ), Middle( (m := s.mids[4]).col, - "Y" - if m.col == "Y" - else MID_DIR_INCREMENT[m.rot], # ("RG" if m.rot == "RB" else "Y"), + "Y" if m.col == "Y" else MID_DIR_INCREMENT[m.rot], ), s.mids[3], Middle( (m := s.mids[1]).col, - "Y" if m.col == "Y" else ("RB" if m.rot == "Y" else "RG"), + "Y" if m.col == "Y" else ("O" if m.rot == "Y" else "R"), ), ), ) @@ -233,8 +204,8 @@ def anticlockwise_twist(s: Skewb, twist: Axis) -> Skewb: return clockwise_twist(clockwise_twist(s, twist), twist) -@pytest.mark.parametrize("start", [desk_start, solved_skewb]) -@pytest.mark.parametrize("axis", axes) +@pytest.mark.parametrize("start", [solved_skewb]) +@pytest.mark.parametrize("axis", AXES) def test_clockwise_twist(start: Skewb, axis: Axis): assert anticlockwise_twist(clockwise_twist(start, axis), axis) == start assert clockwise_twist(anticlockwise_twist(start, axis), axis) == start @@ -252,113 +223,17 @@ def test_clockwise_twist(start: Skewb, axis: Axis): assert clockwise_twist(anticlockwise_twist(s, axis), axis) == s -def test_clockwise_twist_simple(): - assert clockwise_twist(desk_start, "R") == Skewb( - top=( - Corner("R", 0), - Corner("B", 0), - Corner("R", 2), - Corner("G", 2), - ), - bot=( - Corner("B", 0), - Corner("O", 2), - Corner("G", 2), - Corner("O", 2), - ), - mids=( - Middle("B", "Y"), - Middle("R", "Y"), - Middle("Y", "Y"), - Middle("G", "RB"), - Middle("O", "RB"), - ), - ) - - -def test_clockwise_twist_simple2(): - assert clockwise_twist(clockwise_twist(desk_start, "R"), "R") == Skewb( - top=( - Corner("R", 0), - Corner("B", 0), - Corner("O", 1), - Corner("G", 2), - ), - bot=( - Corner("B", 0), - Corner("R", 1), - Corner("G", 0), - Corner("O", 1), - ), - mids=( - Middle("B", "Y"), - Middle("Y", "Y"), - Middle("O", "RG"), - Middle("G", "RB"), - Middle("R", "RB"), - ), - ) - - -def test_clockwise_twist_simple3(): - start = Skewb( - top=( - Corner(col="R", rot=0), - Corner(col="B", rot=0), - Corner(col="O", rot=0), - Corner(col="G", rot=0), - ), - bot=( - Corner(col="G", rot=0), - Corner(col="R", rot=1), - Corner(col="B", rot=2), - Corner(col="O", rot=1), - ), - mids=( - Middle(col="Y", rot="Y"), - Middle(col="O", rot="Y"), - Middle(col="R", rot="RG"), - Middle(col="B", rot="RB"), - Middle(col="G", rot="RG"), - ), - ) - start.assert_valid() - end = Skewb( - top=( - Corner(col="R", rot=0), - Corner(col="B", rot=1), - Corner(col="O", rot=0), - Corner(col="G", rot=0), - ), - bot=( - Corner(col="B", rot=2), - Corner(col="R", rot=2), - Corner(col="G", rot=2), - Corner(col="O", rot=1), - ), - mids=( - Middle(col="O", rot="RG"), - Middle(col="G", rot="RB"), - Middle(col="R", rot="RG"), - Middle(col="B", rot="RB"), - Middle(col="Y", rot="Y"), - ), - ) - end.assert_valid() - assert clockwise_twist(start, "G") == end - - -def apply_twist(start: Skewb, twist: LowerAntiAxis) -> Skewb: - if twist in axes: +def apply_twist(start: Skewb, twist: Twist) -> Skewb: + if twist in AXES: return clockwise_twist(start, twist) return anticlockwise_twist(start, to_clockwise[twist]) -def apply_opposite(s: Skewb, twist: LowerAntiAxis) -> Skewb: +def apply_opposite(s: Skewb, twist: Twist) -> Skewb: return apply_twist(s, to_opposite[twist]) -def apply_twists(start: Skewb, twists: list[LowerAntiAxis]) -> Skewb: +def apply_twists(start: Skewb, twists: list[Twist]) -> Skewb: return reduce(apply_twist, twists, start) @@ -367,7 +242,7 @@ def instructions(twists: list[Axis]) -> str: axis = "R" for twist in twists: while axis != twist: - axis = axes[(1 + axes.index(axis)) % 4] + axis = AXES[(1 + AXES.index(axis)) % 4] out += "L" out += "." @@ -378,15 +253,15 @@ def instructions(twists: list[Axis]) -> str: def breadth_first_search( start: Skewb, is_end: Callable[[Skewb], bool], max_steps: int = 2000000 -) -> list[LowerAntiAxis] | None: +) -> list[Twist] | None: start.assert_valid() if is_end(start): return [] q = deque([start]) # what action got us to this point - skewb_to_twist: dict[Skewb, LowerAntiAxis | None] = {skewb: None for skewb in q} + skewb_to_twist: dict[Skewb, Twist | None] = {skewb: None for skewb in q} - def get_path(end: Skewb) -> list[LowerAntiAxis]: + def get_path(end: Skewb) -> list[Twist]: out = [] s = end while twist := skewb_to_twist[s]: @@ -401,7 +276,7 @@ def breadth_first_search( print(".", end="", flush=True) parent = q.popleft() - for twist in lower_anti_axes: + for twist in TWISTS: child = apply_twist(parent, twist) if child in skewb_to_twist: continue @@ -415,7 +290,7 @@ def breadth_first_search( def test_breadth_first_search(): - for twist in lower_anti_axes: + for twist in TWISTS: assert breadth_first_search( apply_opposite(solved_skewb, twist), is_end=lambda s: s == solved_skewb ) == [twist] @@ -431,15 +306,15 @@ def print_path(path: list[Axis]): def bidirectional_search( start: Skewb, max_steps: int, end: Skewb = solved_skewb -) -> list[LowerAntiAxis] | None: +) -> list[Twist] | None: start.assert_valid() q = deque([start]) q2 = deque([end]) # what action got us to this point - skewb_to_twist: dict[Skewb, LowerAntiAxis | None] = {skewb: None for skewb in q} - skewb_to_twist2: dict[Skewb, LowerAntiAxis | None] = {skewb: None for skewb in q2} + skewb_to_twist: dict[Skewb, Twist | None] = {skewb: None for skewb in q} + skewb_to_twist2: dict[Skewb, Twist | None] = {skewb: None for skewb in q2} - def get_path(meet: Skewb) -> list[LowerAntiAxis]: + def get_path(meet: Skewb) -> list[Twist]: path = [] s = meet while twist := skewb_to_twist[s]: @@ -457,7 +332,7 @@ def bidirectional_search( axis = "R" for twist in twists: while axis != twist: - axis = axes[(1 + axes.index(axis)) % 4] + axis = AXES[(1 + AXES.index(axis)) % 4] out += "L" out += "." @@ -465,7 +340,7 @@ def bidirectional_search( out = out.replace("LLL", "J") return out - def on_meet(meet: Skewb) -> list[LowerAntiAxis]: + def on_meet(meet: Skewb) -> list[Twist]: path = get_path(meet) assert apply_twists(start, path) == end return path @@ -478,7 +353,7 @@ def bidirectional_search( print(".", end="", flush=True) parent2 = q2.popleft() - for twist in lower_anti_axes: + for twist in TWISTS: child = apply_opposite(parent2, twist) if child in skewb_to_twist2: continue @@ -488,7 +363,7 @@ def bidirectional_search( q2.append(child) parent = q.popleft() - for twist in lower_anti_axes: + for twist in TWISTS: child = apply_twist(parent, twist) if child in skewb_to_twist: @@ -551,30 +426,30 @@ def element_multiply(a: list[int], b: list[int]) -> list[int]: def test_heuristic(): - assert heuristic(desk_start) < heuristic(solved_skewb) + assert heuristic(apply_twist(solved_skewb, "O")) < heuristic(solved_skewb) def random_skewb(seed: int = 4, twists: int = 20) -> Skewb: return apply_twists(solved_skewb, random_skewb_twists(seed, twists)) -def random_skewb_twists(seed: int = 4, twists: int = 20) -> list[LowerAntiAxis]: - out: list[LowerAntiAxis] = [] +def random_skewb_twists(seed: int = 4, twists: int = 20) -> list[Twist]: + out: list[Twist] = [] rng = random.Random(seed) ax_i = rng.randint(0, 3) for _ in range(twists): - twist = axes[ax_i] + twist = AXES[ax_i] if rng.getrandbits(1): twist = to_opposite[twist] out.append(twist) - ax_i = (ax_i + rng.randint(1, 3)) % len(axes) + ax_i = (ax_i + rng.randint(1, 3)) % len(AXES) return out -def double_clockwise_to_anticlockwise(twists: list[Axis]) -> list[LowerAntiAxis]: +def double_clockwise_to_anticlockwise(twists: list[Axis]) -> list[Twist]: i = 0 n = len(twists) - out: list[LowerAntiAxis] = [] + out: list[Twist] = [] while i < n: twist = twists[i] if i < n - 1 and twist == twists[i + 1]: @@ -596,9 +471,7 @@ def double_clockwise_to_anticlockwise(twists: list[Axis]) -> list[LowerAntiAxis] ("RORR", "ROr"), ], ) -def test_double_clockwise_to_anticlockwise( - twists: list[Axis], want: list[LowerAntiAxis] -): +def test_double_clockwise_to_anticlockwise(twists: list[Axis], want: list[Twist]): assert double_clockwise_to_anticlockwise(twists) == list(want) @@ -625,8 +498,8 @@ def shelve_it(file_name): def get_paths_from_heuristic( start: Skewb, heuristic_permutation: list[int] -) -> list[list[LowerAntiAxis]]: - out: list[list[LowerAntiAxis]] = [] +) -> list[list[Twist]]: + out: list[list[Twist]] = [] s = start mask = [0 for _ in heuristic_permutation] total_path_length = 0 @@ -742,9 +615,16 @@ if __name__ == "__main__": # print(f"{hp=} {evaluate_permutation(hp, seed=200, sample_size=200)=}") hp = [10, 0, 1, 2, 3, 8, 9, 11, 12, 4, 5, 6, 7] - path = get_paths_from_heuristic( - Skewb(top=(O0, B0, R0, G0), bot=(B2, R0, G1, O0), mids=(BY, YY, GY, RY, ORB)), - hp, + print( + bidirectional_search( + Skewb( + top=(G0, O0, B0, R0), bot=(B1, O1, G2, R1), mids=(OO, YY, RO, GR, BO) + ), + max_steps=1000000, + ) ) - print(path) + # path = get_paths_from_heuristic( + # Skewb(top=(G0, O0, B0, R0), bot=(B1, O1, G2, R1), mids=(OR, YY, RO, GR, BO)), + # hp, + # ) # RBOG