Created
September 20, 2024 01:10
-
-
Save dsetzer/127653d9efe68a537bedf57e9d8ecf5a to your computer and use it in GitHub Desktop.
Prototype version two for solving 4x4x4 sudoku cube
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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