rubiks

Solves a 2D Rubiks cube so that all rows have the same colour
git clone git://mattcarlson.org/repos/rubiks.git
Log | Files | Refs | README

commit 5de2e30caa9bb60627a318c977d3e6f6e251b6a9
Author: Matthew Carlson <matt@mcarlson.xyz>
Date:   Mon, 15 Mar 2021 21:43:34 -0400

Initial commit

Diffstat:
AREADME.md | 5+++++
Arubiks.py | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 143 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,5 @@ +# Rubiks + +Solves a 2D Rubiks cube so that all rows have the same colour + +![rubiks](https://mcarlson.xyz/img/rubiks.png) diff --git a/rubiks.py b/rubiks.py @@ -0,0 +1,138 @@ +import numpy as np +from operator import attrgetter + +# Rubik's Cube +# Cubies: Individual cubes stored in 2D array ('R' = Red, 'G' = Green, 'B' = Blue) +# Weight: f = g + h value calculated via heuristic() and cost in solve() +# Move: Description of move performed (e.g. "Row 0 Left") +# Parent: Cube from which this cube is derived +class Rubiks: + def __init__(self, cubies, weight, move, parent): + self.cubies = cubies + self.weight = weight + self.move = move + self.parent = parent + + # Print cubies as move performed to get there + def __str__(self): + return '\n'.join(['|'.join([str(cubies) for cubies in row]) for row in + self.cubies]) + '\nMove: ' + (self.move) + +# Move Rubik's Cube by "rolling" row left or right OR column up and down +# E.g. R G B rolled left would be G B R +# and +# R G +# G rolled up would be B +# B R +def move(cubies, pos, dir): + + # Duplicate cubies so that other cubes won't be altered + cubies = cubies.copy() + + if dir == 'Left': + cubies[pos,:] = np.roll(cubies[pos,:], 2) + elif dir == 'Right': + cubies[pos,:] = np.roll(cubies[pos,:], -2) + elif dir == 'Up': + cubies[:,pos] = np.roll(cubies[:,pos], 2) + elif dir == 'Down': + cubies[:,pos] = np.roll(cubies[:,pos], -2) + + return cubies + +# Calculate h value (number of cubies out of place, i.e. Hamming distance) +# cost g calculated in solve() +def heuristic(cubies, goal): + h = 0 + # If cubie not in right place add 1 to h + for i,j in np.ndindex(goal.shape): + if (cubies[i, j] != goal[i, j]): + h += 1 + return h; + +# Helper function to *efficiently* find move in opened/closed set +def find(list, key): + for item in list: + if np.array_equal(item.cubies, key.cubies): return 1 + return 0 + +# A* search algorithm +def solve(rubiks, goal): + # Possible moves + dirs = ['Left', 'Right', 'Up', 'Down'] + # The A* search algorithm open priority queue + open = [] + # Start state automatically placed in closed + closed = [rubiks] + + node = rubiks + + cost = 0 + + # Heuristic of zero means goal state reached + while heuristic(node.cubies, goal) > 0: + cost += 1 + + # Generate all successors for node + # I.e. For row/column zero, one, or two you can move up, down, left, right + for dir in dirs: + for pos in range(np.shape(node.cubies)[0]): + cubies = move(node.cubies, pos, dir) + adjacent = Rubiks(cubies, heuristic(cubies, goal) + cost, ('Column ' + if dir == 'Up' or dir == 'Down' else 'Row ') + str(pos) + ' ' + dir, + node) + + # Skip if in open or closed with lower weight + if find(open, adjacent) or find(closed, adjacent): + continue + + # Else add to open + open.append(adjacent) + + # Sort open by minimum weight + # Node has least weight and is popped off from open set + open.sort(key=lambda move: move.weight) + node = open.pop(0) + + # If node's parent is in closed remove nodes following it, reset cost, + # and check open for stragglers + for i, c in enumerate(closed): + if c == node.parent: + closed = closed[:i + 1] + cost = i + 1 + for o in open: + if o.parent not in closed: + open.pop(0) + + # Add node to closed set after examining it + closed.append(node) + + # Print moves needed to get to goal state + for c in closed: + print(c) + +def main(): + + # We want to get this to goal + start = np.array([['R', 'G', 'G'], + ['G', 'B', 'R'], + ['B', 'R', 'B']]) + + + # Here is what solved 2D Rubik's Cube looks like + goal = np.array([['R', 'R', 'R'], + ['G', 'G', 'G'], + ['B', 'B', 'B']]) + + # Default cube will have these attributes: + # Cubies: start + # Heuristic: See heuristic() above + # No parent, it is the first one + # No move either, just call it Begin + rubiks = Rubiks(start, heuristic(start, goal), 'Begin', None) + + # Solve the cube! + solve(rubiks, goal) + +if __name__ == "__main__": + main()