Skip to content

Commit

Permalink
add a star solver
Browse files Browse the repository at this point in the history
  • Loading branch information
osbm committed Feb 24, 2023
1 parent a4ffa23 commit cf32852
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 13 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
143 changes: 143 additions & 0 deletions docs/examples/intro.ipynb
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions rubics_cube/__init__.py
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
49 changes: 39 additions & 10 deletions rubics_cube/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
104 changes: 104 additions & 0 deletions rubics_cube/heuristics.py
Original file line number Diff line number Diff line change
@@ -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!")

0 comments on commit cf32852

Please sign in to comment.