Skip to content

Instantly share code, notes, and snippets.

@dsetzer
Created September 20, 2024 01:10
Show Gist options
  • Select an option

  • Save dsetzer/127653d9efe68a537bedf57e9d8ecf5a to your computer and use it in GitHub Desktop.

Select an option

Save dsetzer/127653d9efe68a537bedf57e9d8ecf5a to your computer and use it in GitHub Desktop.
Prototype version two for solving 4x4x4 sudoku cube
from enum import Enum
from typing import List, Dict, Tuple
from collections import Counter
import random
import subprocess
class TileType(Enum):
CORNER = 'corner'
EDGE_A = 'edge_a'
EDGE_B = 'edge_b'
CENTER = 'center'
class CubeTile:
TILE_TYPES = {
TileType.CORNER: {1, 4, 13, 16},
TileType.EDGE_A: {2, 8, 9, 15},
TileType.EDGE_B: {3, 5, 12, 14},
TileType.CENTER: {6, 7, 10, 11}
}
def __init__(self, number: int, current_face: str, current_position: int):
self.number = number
self.color = None
self.tile_type = self._detect_tile_type(current_position)
self.current_face = current_face
self.current_position = current_position
self.expected_face = None
self.expected_position = number
@classmethod
def _detect_tile_type(cls, current_position: int) -> TileType:
for tile_type, positions in cls.TILE_TYPES.items():
if current_position in positions:
return tile_type
raise ValueError(f"Invalid face position: {current_position}. Valid positions are: {set.union(*cls.TILE_TYPES.values())}")
def __repr__(self):
return f"Tile({self.number}, {self.tile_type}, {self.color}, {self.current_face}, {self.current_position}, {self.expected_face}, {self.expected_position})"
class CubeFace:
COLOR_MAP = {'U': 'W', 'R': 'R', 'F': 'G', 'D': 'Y', 'L': 'O', 'B': 'B'}
NEIGHBOR_MAP = {
'U': ['F', 'R', 'B', 'L'],
'R': ['U', 'F', 'D', 'B'],
'F': ['U', 'R', 'D', 'L'],
'D': ['F', 'R', 'B', 'L'],
'L': ['U', 'F', 'D', 'B'],
'B': ['U', 'R', 'D', 'L']
}
def __init__(self, side: str, tile_data: List[int] = None):
# if no tile data is provided, assume we're creating a stock cube
if tile_data is None or len(tile_data) != 16:
tile_data = list(range(1, 17))
self.side = side
self.color = self.COLOR_MAP[side]
self.current_tiles = [CubeTile(number, side, i + 1) for i, number in enumerate(tile_data)]
self.neighbor_sides = self.NEIGHBOR_MAP[side]
self.neighbor_colors = [self.COLOR_MAP[neighbor] for neighbor in self.neighbor_sides]
# find tiles that are in their correct positions
self.solved_tiles = {i: None for i in range(1, 17)}
# for tile in self.current_tiles:
# if tile.current_position == tile.expected_position:
# tile.color = self.color
# tile.expected_face = self.side
# self.solved_tiles[tile.number] = tile
def __repr__(self):
return f"Face({self.side}, {self.color})"
class Cube:
KOCIEMBA_ORDER = ['U', 'R', 'F', 'D', 'L', 'B']
FACE_MAP = {'U': 0, 'R': 1, 'F': 2, 'D': 3, 'L': 4, 'B': 5}
COLOR_MAP = {'U': 'W', 'R': 'R', 'F': 'G', 'D': 'Y', 'L': 'O', 'B': 'B'}
NEIGHBOR_MAP = CubeFace.NEIGHBOR_MAP
EDGE_NEIGHBOR_MAP = {
'U': { 2: ('B', 3), 3: ('B', 2), 5: ('L', 3), 9: ('L', 2), 8: ('R', 2), 12: ('R', 3), 14: ('F', 3), 15: ('F', 2) },
'R': { 2: ('U', 8), 3: ('U', 12), 5: ('F', 8), 9: ('F', 12), 8: ('D', 8), 12: ('D', 12), 14: ('B', 9), 15: ('B', 5) },
'F': { 2: ('U', 14), 3: ('U', 15), 5: ('L', 12), 9: ('L', 8), 8: ('R', 5), 12: ('R', 9), 14: ('D', 15), 15: ('D', 14) },
'D': { 2: ('F', 15), 3: ('F', 14), 5: ('L', 15), 9: ('L', 14), 8: ('R', 14), 12: ('R', 15), 14: ('B', 15), 15: ('B', 14) },
'L': { 2: ('U', 5), 3: ('U', 9), 5: ('B', 12), 9: ('B', 8), 8: ('F', 9), 12: ('F', 5), 14: ('D', 5), 15: ('D', 9) },
'B': { 2: ('U', 3), 3: ('U', 2), 5: ('R', 15), 9: ('R', 14), 8: ('L', 9), 12: ('L', 5), 14: ('D', 3), 15: ('D', 2) }
}
CORNER_NEIGHBOR_MAP = {
'U': { 1: [('L', 1), ('B', 4)], 4: [('B', 1), ('R', 4)], 13: [('L', 4), ('F', 1)], 16: [('F', 4), ('R', 1)] },
'R': { 1: [('U', 16), ('F', 4)], 4: [('U', 4), ('B', 1)], 13: [('F', 16), ('D', 13)], 16: [('D', 4), ('B', 13)] },
'F': { 1: [('U', 13), ('L', 4)], 4: [('U', 16), ('R', 1)], 13: [('L', 16), ('D', 16)], 16: [('R', 13), ('D', 13)] },
'D': { 1: [('F', 13), ('L', 13)], 4: [('R', 16), ('F', 16)], 13: [('B', 16), ('L', 16)], 16: [('R', 13), ('B', 13)] },
'L': { 1: [('U', 1), ('B', 4)], 4: [('F', 1), ('U', 13)], 13: [('D', 1), ('B', 16)], 16: [('D', 16), ('F', 13)] },
'B': { 1: [('R', 4), ('U', 4)], 4: [('U', 1), ('L', 1)], 13: [('D', 4), ('R', 16)], 16: [('L', 13), ('D', 13)] }
}
def __init__(self, numbered_state: List[List[int]]):
self.faces = {side: CubeFace(side, numbered_state[face_index]) for side, face_index in self.FACE_MAP.items()}
# self.assign_colors_to_tiles()
def generate_standard_state(self) -> str:
self.assign_colors_to_tiles()
for side in self.KOCIEMBA_ORDER:
for i, tile in enumerate(self.faces[side].current_tiles):
if tile.color is None:
print(f"Tile with no color found: Side {side}, Index {i}")
return ''.join(tile.color for side in self.KOCIEMBA_ORDER for tile in self.faces[side].current_tiles)
def assign_colors_to_tiles(self):
for current_face in self.faces.values():
for tile in current_face.current_tiles:
if tile.number == tile.current_position:
if tile.color is None:
tile.color = current_face.color
tile.expected_face = current_face.side
print('Tile', f'{tile.current_face}{tile.current_position}', 'already in solved position at ', f'{tile.expected_face}{tile.current_position}')
continue
elif current_face.solved_tiles[tile.number] is not None:
solved_tile = current_face.solved_tiles[tile.number]
if solved_tile.color is None:
solved_tile.color = current_face.color
solved_tile.expected_face = current_face.side
print('Tile', f'{tile.current_face}{tile.current_position}', 'already solved at ', f'{solved_tile.current_face}{solved_tile.current_position}')
continue
else:
located_tile = None
if tile.tile_type == TileType.EDGE_A or tile.tile_type == TileType.EDGE_B:
located_tile = self.locate_valid_unassigned_edge_tile(current_face, tile.current_position)
elif tile.tile_type == TileType.CORNER:
located_tile = self.locate_valid_unassigned_corner_tile(current_face, tile.current_position)
else:
located_tile = self.locate_valid_unassigned_center_tile(current_face, tile.current_position)
if located_tile:
located_tile.color = current_face.color
located_tile.expected_face = current_face.side
current_face.solved_tiles[tile.number] = located_tile
print('Tile', f'{tile.current_face}{tile.current_position}', 'solved at ', f'{located_tile.current_face}{located_tile.current_position}')
else:
print('Tile', f'{tile.current_face}{tile.current_position}', 'could not be solved')
def locate_valid_unassigned_edge_tile(self, expected_face: CubeFace, expected_position: int) -> CubeTile:
print(f"\nAttempting to locate unassigned edge tile for side {expected_face.side} at index {expected_position}")
expected_tile = self.faces[expected_face.side].current_tiles[expected_position - 1]
if not expected_tile or expected_tile.tile_type not in [TileType.EDGE_A, TileType.EDGE_B]:
print(f"Expected tile {expected_face.side}{expected_position} is not an edge tile")
return None
expected_neighbor = self.EDGE_NEIGHBOR_MAP[expected_face.side][expected_position]
expected_edge_color = self.faces[expected_neighbor[0]].color
expected_edge_number = expected_neighbor[1]
print(f"Expected edge color is {expected_edge_color} and expected edge number is {expected_edge_number}")
for face in self.faces.values():
for tile in face.current_tiles:
if tile.tile_type == expected_tile.tile_type and not tile.color and tile.number == expected_position:
adjacent_tile = self.EDGE_NEIGHBOR_MAP[tile.current_face][tile.current_position]
adjacent_tile_color = self.faces[adjacent_tile[0]].current_tiles[adjacent_tile[1]-1].color
adjacent_tile_number = adjacent_tile[1]
print(f"Found tile {tile.current_face}{tile.current_position} with color {adjacent_tile_color} and number {adjacent_tile_number}")
if (adjacent_tile_color is None or adjacent_tile_color == expected_edge_color) and adjacent_tile_number == expected_edge_number:
print(f"Tile {tile.current_face}{tile.current_position} matches expected edge")
return tile
print(f"Could not find a valid edge tile for side {expected_face.side} at index {expected_position}")
return None
def locate_valid_unassigned_corner_tile(self, expected_face: CubeFace, expected_position: int) -> CubeTile:
print(f"\nAttempting to corner tile {expected_face.side}{expected_position}")
expected_tile = self.faces[expected_face.side].current_tiles[expected_position - 1]
if not expected_tile or expected_tile.tile_type != TileType.CORNER:
print(f"Fail: Expected tile {expected_face.side}{expected_position} is not an edge tile")
return None
expected_neighbors = self.CORNER_NEIGHBOR_MAP[expected_face.side][expected_position]
expected_corner_colors = [self.faces[neighbor[0]].color for neighbor in expected_neighbors]
expected_neighbor_numbers = [neighbor[1] for neighbor in expected_neighbors]
print('Expected corner colors:', expected_corner_colors)
print('Expected neighbor numbers:', expected_neighbor_numbers)
for face in self.faces.values():
for tile in face.current_tiles:
if tile.tile_type == TileType.CORNER and not tile.color and tile.number == expected_position:
print(f"Found tile {tile.current_face}{tile.current_position} with number {expected_position}")
# get adjacent tiles from neighboring faces
adjacent_tiles = self.CORNER_NEIGHBOR_MAP[tile.current_face][tile.current_position]
# make sure the adjacent tile colors are either none or the expected_corner_colors
adjacent_tile_colors = [self.faces[neighbor[0]].current_tiles[neighbor[1]-1].color for neighbor in adjacent_tiles]
adjacent_tile_numbers = [self.faces[neighbor[0]].current_tiles[neighbor[1]-1].number for neighbor in adjacent_tiles]
print(f'Tile {tile.current_face}{tile.current_position} has adjacent colors {adjacent_tile_colors} and numbers {adjacent_tile_numbers}')
if all(color is None or color in expected_corner_colors for color in adjacent_tile_colors) and all(number in expected_neighbor_numbers for number in adjacent_tile_numbers):
print(f"Tile {tile.current_face}{tile.current_position} matches expected corner")
return tile
print(f"No valid corner tile found for side {expected_face.side} at index {expected_position}")
return None
def locate_valid_unassigned_center_tile(self, expected_face: CubeFace, expected_position: int) -> CubeTile:
print(f"Attempting to locate center tile {expected_face.side}{expected_position}")
expected_tile = self.faces[expected_face.side].current_tiles[expected_position - 1]
if not expected_tile or expected_tile.tile_type != TileType.CENTER:
print(f"Fail: Expected tile {expected_face.side}{expected_position} is not a center tile")
return None
for face in self.faces.values():
print(f"Checking face {face.side}")
for tile in face.current_tiles:
if tile.tile_type == TileType.CENTER and not tile.color and tile.number == expected_position:
print(f"Found tile {tile.current_face}{tile.current_position} with number {expected_position}")
return tile
print(f"No valid center tile found for side {expected_face.side} at index {expected_position}")
return None
def get_state(self) -> List[List[int]]:
return [[tile.number for tile in current_face.current_tiles] for current_face in self.faces.values()]
def print_cube_state(self):
for side in self.KOCIEMBA_ORDER:
face = self.faces[side]
print(f"Face {face.side}:")
for i, tile in enumerate(face.current_tiles, 1):
print(f"{tile.color}({tile.number})", end=" ")
if i % 4 == 0:
print()
print()
def apply_solution(self, solution: str):
moves = solution.split()
for move in moves:
face = move[0]
direction = move[1] if len(move) > 1 else ''
self.rotate_face(face, direction)
def rotate_face(self, face: str, direction: str):
rotation_map = {
'': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
'2': [13, 14, 15, 16, 9, 10, 11, 12, 5, 6, 7, 8, 1, 2, 3, 4],
"'": [4, 1, 2, 3, 8, 5, 6, 7, 12, 9, 10, 11, 16, 13, 14, 15]
}
rotated_tiles = [self.faces[face].current_tiles[i-1] for i in rotation_map[direction]]
self.faces[face].current_tiles = rotated_tiles
neighbors = self.NEIGHBOR_MAP[face]
if face in ['U', 'D']:
affected_positions = [1, 2, 3, 4] if face == 'U' else [13, 14, 15, 16]
elif face in ['R', 'L']:
affected_positions = [4, 8, 12, 16] if face == 'R' else [1, 5, 9, 13]
else: # F or B
affected_positions = [13, 14, 15, 16] if face == 'F' else [1, 2, 3, 4]
tiles_to_move = [
self.faces[neighbors[0]].current_tiles[pos-1] for pos in affected_positions
]
for i in range(3):
current_face = neighbors[i]
next_face = neighbors[(i+1) % 4]
for j, pos in enumerate(affected_positions):
self.faces[next_face].current_tiles[pos-1], tiles_to_move[j] = \
tiles_to_move[j], self.faces[next_face].current_tiles[pos-1]
for i, pos in enumerate(affected_positions):
self.faces[neighbors[0]].current_tiles[pos-1] = tiles_to_move[i]
# Example usage and testing
if __name__ == "__main__":
from cube_state import CubeState
cubeState = CubeState()
print("\nScrambling the cube...")
cubeState.scramble()
cubeState.debug_info()
example_cube_state = cubeState.get_state()
# example_cube_state = [
# [13, 15, 5, 16, 5, 7, 10, 8, 9, 10, 6, 14, 13, 14, 15, 1],
# [13, 15, 14, 4, 14, 6, 11, 15, 15, 6, 7, 12, 1, 3, 9, 16],
# [1, 15, 3, 4, 12, 11, 7, 9, 2, 7, 10, 3, 4, 14, 9, 1],
# [16, 2, 14, 1, 12, 7, 6, 2, 9, 11, 11, 12, 1, 12, 9, 16],
# [16, 8, 12, 16, 5, 6, 7, 2, 8, 10, 6, 3, 13, 3, 8, 4],
# [4, 2, 3, 4, 5, 11, 11, 2, 8, 10, 10, 5, 13, 5, 8, 13]
# ]
default_cube_state = [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
]
cube = Cube(example_cube_state)
result = cube.generate_standard_state()
print("\nKociemba state:", result)
color_counts = Counter(result)
print("\nColor counts:\n" + "\n".join(f"{color}: {count}" for color, count in color_counts.items()))
print("\nCube state visualization:")
cube.print_cube_state()
try:
command = f'./rubiks-cube-solver.py --state {result}'
process = subprocess.run(command, shell=True, capture_output=True, text=True)
print("\nCommand Output:")
print(process.stdout)
solution = process.stdout.strip()
print("\nApplying solution to the cube...")
cube.apply_solution(solution)
print("\nCube state after applying solution:")
cube.print_cube_state()
print("\nVerifying if the cube is solved...")
is_solved = all(all(tile.number == tile.current_position for tile in face.current_tiles) for face in cube.faces.values())
print("Cube solved:", is_solved)
except Exception as e:
print("Error running command:", e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment