diff --git a/README.md b/README.md index 2f2e5d1..58978ca 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ # To-Do -- [ ] implement shuffler -- [ ] implement an a* search algorithm to solve the cube +- [x] implement shuffler +- [x] implement an a* search algorithm to solve the cube +- [x] implement heuristic functions for the a* search algorithm - [ ] animation generation -- [ ] implement heuristic functions for the a* search algorithm - [ ] implement solvability check diff --git a/docs/examples/intro.ipynb b/docs/examples/intro.ipynb new file mode 100644 index 0000000..a0dcabb --- /dev/null +++ b/docs/examples/intro.ipynb @@ -0,0 +1,143 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mWARNING: You are using pip version 22.0.4; however, version 23.0.1 is available.\n", + "You should consider upgrading via the '/home/osman/Documents/github/rubics-cube/.venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "!pip install --no-cache-dir -q -U ../.." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ╔═U════╗\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;33myy\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + "╔═L════╬═F════╬═R════╦═B════╗\n", + "║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[1;31mrr\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[38;5;214moo\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[1;31mrr\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[38;5;214moo\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[38;5;214moo\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "╚══════╬═D════╬══════╩══════╝\n", + " ║\u001b[38;5;255mww\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ╚══════╝\n", + "current best cube value: 24\n", + "Solution length: 3 moves\n", + " ╔═U════╗\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + "╔═L════╬═F════╬═R════╦═B════╗\n", + "║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "╚══════╬═D════╬══════╩══════╝\n", + " ║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ╚══════╝\n", + " ╔═U════╗\n", + " ║\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\n", + "╔═L════╬═F════╬═R════╦═B════╗\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[1;34mbb\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;32mgg\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "╚══════╬═D════╬══════╩══════╝\n", + " ║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ╚══════╝\n", + " ╔═U════╗\n", + " ║\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + " ║\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m\u001b[1;34mbb\u001b[0m║\n", + "╔═L════╬═F════╬═R════╦═B════╗\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "║\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m\u001b[38;5;214moo\u001b[0m║\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m\u001b[38;5;255mww\u001b[0m║\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m\u001b[1;31mrr\u001b[0m║\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m\u001b[1;33myy\u001b[0m║\n", + "╚══════╬═D════╬══════╩══════╝\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ║\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m\u001b[1;32mgg\u001b[0m║\n", + " ╚══════╝\n" + ] + } + ], + "source": [ + "import rubics_cube as rc\n", + "\n", + "cube = rc.Cube(size=3, show_letter=True)\n", + "cube.shuffle(4)\n", + "\n", + "cube.print()\n", + "\n", + "heuristic = rc.same_color_amount\n", + "solver = rc.AStarSolver(cube, heuristic)\n", + "\n", + "solution = solver.solve()\n", + "\n", + "print(\"Solution length:\", len(solution), \"moves\")\n", + "\n", + "for move in solution:\n", + " cube.make_move(move)\n", + " cube.print()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "cfcf6a851aa7abbcb53a758be308e0956bbf6a72dc19c18fa073aa2d68ce39af" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/rubics_cube/__init__.py b/rubics_cube/__init__.py index 81f6aad..1f753d6 100644 --- a/rubics_cube/__init__.py +++ b/rubics_cube/__init__.py @@ -1,4 +1,5 @@ from .cube import Cube +from .heuristics import AStarSolver, same_color_amount # make sure that the version is available in the package namespace module_path = __file__.split("__init__.py", maxsplit=1)[0] diff --git a/rubics_cube/cube.py b/rubics_cube/cube.py index a6a665e..bbee7bc 100644 --- a/rubics_cube/cube.py +++ b/rubics_cube/cube.py @@ -66,12 +66,28 @@ def __init__( if scrambled: self.scramble() + # make from_combinations function to initialize the cube from a given combination + @classmethod + def from_combinations(cls, combinations): + # sanity checks for the combinations + assert combinations.shape[0] == 6, "There should be 6 faces." + assert combinations.shape[1] == combinations.shape[2], "The cube should be cubic." + # every element chould be [w, o, g, r, b, y] + assert np.all( + np.isin(combinations, ["w", "o", "g", "r", "b", "y"]) + ), "Every element should be in [w, o, g, r, b, y]." + + + cube = cls(size=combinations.shape[1], scrambled=False) + cube.combinations = combinations + return cube + def is_solved(self): for face_index in range(6): - first_color = self.combinations[face_index][0] + first_color = self.combinations[face_index][0][0] for i in range(self.size): for j in range(self.size): - if self.combinations[face_index][i * self.size + j] != first_color: + if self.combinations[face_index][i][j] != first_color: return False return True @@ -365,19 +381,32 @@ def get_possible_moves(self): *middle_layer_moves, ] - def _print_letter(self, letter: str): - color = self.console_colors[letter] - print(color, end="") + def shuffle(self, num_moves: int = 20): + """ + Shuffle the cube + """ + possible_moves = self.get_possible_moves() + for _ in range(num_moves): + move = random.choice(possible_moves) + self.make_move(move, print_cube=False) + + def _print_letter(self, letter: str, color: bool = True): + if color: + color = self.console_colors[letter] + print(color, end="") if self.show_letter: print(letter * 2, end="") else: print("██", end="") - print(self.console_colors["reset"], end="") + if color: + print(self.console_colors["reset"], end="") - def print(self): + def print(self, terminal_color: bool = True): # draw top pipes of the top face of cube + assert terminal_color or self.show_letter, "you should either show letter or color, this would be invisible otherwise" + print( " " * (self.size * 2 + 1), Pipes.top_left, @@ -392,7 +421,7 @@ def print(self): print(" " * (self.size * 2 + 1), end="") print(Pipes.vertical, end="") for j in range(self.size): - self._print_letter(self.combinations[0][i][j]) + self._print_letter(self.combinations[0][i][j], color=terminal_color) print(Pipes.vertical) # draw 3 faces of cube next to each other @@ -421,7 +450,7 @@ def print(self): for j in range(1, 5): print(Pipes.vertical, end="") for k in range(self.size): - self._print_letter(self.combinations[j][i][k]) + self._print_letter(self.combinations[j][i][k], color=terminal_color) print(Pipes.vertical) print( @@ -443,7 +472,7 @@ def print(self): print(" " * (self.size * 2 + 1), end="") print(Pipes.vertical, end="") for j in range(self.size): - self._print_letter(self.combinations[5][i][j]) + self._print_letter(self.combinations[5][i][j], color=terminal_color) print(Pipes.vertical) print( diff --git a/rubics_cube/heuristics.py b/rubics_cube/heuristics.py new file mode 100644 index 0000000..7d5dc21 --- /dev/null +++ b/rubics_cube/heuristics.py @@ -0,0 +1,104 @@ +import numpy as np +from .cube import Cube +from typing import Callable + +def same_color_amount(combinations: np.array) -> int: + """Sum of the largest amount of same colors on a face. + On a 5x5x5 cube largest possible amount is 6*5*5 = 150. + *bigger the better* + + Args: + combinations (np.array): A combination of a cube. shape: (6, n, n) + + Returns: + int: Value of the heuristic. + """ + + # check if the colors are the same on the same face + same_color_amount = 0 + for face in combinations: + value_counts = np.unique(face, return_counts=True)[1] + same_color_amount += np.max(value_counts) + + # but we need a function that returns a smaller value for a better solution + # so we subtract the value from the maximum possible value + max_possible_value = 6*combinations.shape[1]*combinations.shape[2] + return max_possible_value - same_color_amount + + + +class AStarSolver(): + def __init__(self, cube: Cube, heuristic: Callable): + """Initialize the solver. + + Args: + combinations (np.array): A combination of a cube. + """ + self.cube = cube + self.heuristic = heuristic + self.possible_moves = cube.get_possible_moves() # possible moves are constant + + # lets remove rotational moves from the possible moves + self.possible_moves = [move for move in self.possible_moves if not (("x" in move) or ("y" in move) or ("z" in move))] + + def make_str(self, combinations: np.array) -> str: + """Make a string from the combinations. + + Args: + combinations (np.array): A combination of a cube. + + Returns: + str: The string. + """ + return "".join(["".join(x_row) for face in combinations for x_row in face]) + + + def solve(self) -> str: + """Solve the cube. + + Returns: + str: The solution. + """ + if self.cube.is_solved(): + print("Cube is already solved!") + return "" + + # current combinations of the cube + initial_combinations = self.cube.combinations.copy() + + visited = [str(initial_combinations)] + queue = [(initial_combinations, (), self.heuristic(initial_combinations))] + # queue elements in the form of (combinations, path, value) + + print("current best cube value: ", queue[0][2]) + + + while queue: + # sort the queue by the value of the elements + + queue = sorted(queue, key=lambda x: len(x[1])+ x[2]) # length of the path + evaluation value + + # get the element with the lowest value + combinations, path, value = queue.pop(0) + + # check if the cube is solved + new_cube = Cube.from_combinations(combinations) + + if new_cube.is_solved(): + return path + + for move in self.possible_moves: + child_cube = Cube.from_combinations(combinations.copy()) + child_cube.make_move(move) + new_combinations = child_cube.combinations + # check if the new combinations are already visited + if str(new_combinations) not in visited: + visited.append(str(new_combinations)) + queue_element = (new_combinations.copy(), (*path, move), self.heuristic(new_combinations)) + queue.append(queue_element) + + if child_cube.is_solved(): + return queue_element[1] + + raise ValueError("Invalid cube!") +