Simulating Rubik Cube Actions with Java
Let’s work on something fun today, simulating a rubik’s cube. This is the precursor to my next topic to provide the solution to solve rubik’s cube.
Modelling
There can be many ways we can simulate the rubik’s cube model as a class, but my goal here is to make it as simple and easy to understand as possible. A seemingly more scientific way is to have 3 dimensional data, but that will involve some 3D maths which can get confusing, so I’m not going to do that. Instead, I will break down the rubik’s cube into 6 sides, and lay it flat.
So we declare a class named RubikSide
to model it, with the size of the cube and the value for each position to represent the color. We are just doing a 3x3 rubik’s cube, but the code should be able to handle different sizes. Instead of saving the 6 colors, we just use numbers 1 - 6 to represent the colors - red, blue, orange, green, white, and yellow.
public class RubikSide implements Cloneable{
private final int size;
private int[][] values;
public RubikSide(int size, int value){
this.size = size;
int[] dimension = IntStream.generate(() -> value).limit(size).toArray();
values = IntStream.range(0, size)
.boxed()
.map(i -> dimension.clone())
.toArray(int[][]::new);
}
}
The values for each side are stored in the 2D array named values
. The 1st dimension is the row, and the 2nd dimension is the column.
As we combine all 6 sides to form the rubik’s cube, we define that the position should be as such when laid out flat. This is very important to our later works, especially when we turn the columns up and down.
public class RubikCube{
private RubikSide main;
private RubikSide right;
private RubikSide left;
private RubikSide back;
private RubikSide top;
private RubikSide bottom;
private int size;
public RubikCube(int size){
this.size = size;
main = new RubikSide(size, 1);
right = new RubikSide(size, 2);
back = new RubikSide(size, 3);
left = new RubikSide(size, 4);
top = new RubikSide(size, 5);
bottom = new RubikSide(size, 6);
}
}
Basic Functions
To prepare for the actions we can perform on the rubik’s cube, we should create some methods to get and set the rows and columns of each RubikSide
. Getting the rows is pretty straightforward, and getting the column just need a little manipulation.
public int[] getRow(int row){
return values[row];
}
public int[] getCol(int col){
return IntStream.range(0, size)
.map(i -> values[i][col])
.toArray();
}
Setting the values is similarly almost the same.
public void setRow(int row, int[] newValues){
values[row] = newValues;
}
public void setCol(int col, int[] newValues){
IntStream.range(0, size).forEach(i -> values[i][col] = newValues[i]);
}
Actions
Now we are ready to create the actions we can perform on the rubik’s cube. My strategy for simplification is to only target the actions that can be perform on the main
side. Without specifying the rows and columns, which should be dynamic, as we want to cater to different rubik’s cube sizes (3x3, 4x4, 5x5), I can summarize all the actions available for 1 side to be as follows:
- Turn row
X
left - Turn row
X
right - Turn column ‘Y’ up
- Turn column ‘Y’ down
- Rotate
main
side clockwise - Rotate
main
side anti-clockwise.
Turning Row
Turning row left and right is the easiest, I just need to reassign the values of each side. For example, turning a row to the left is just reassigning the values of the row on the main
side with the values of the same row on the right
side. Then we replace the values of the row on the right
side with the values of the same row on the back
side. And we replace the values of the row on the back
side with the values of the row on the left
side. Lastly, before we begin all these, we should have a copy of the values of the row on the main
side, so that we can replace the values of the row on the left
side with the values of the row on the main
side. However, we need to make sure that if the top or bottom row is involved, we need to rotate the respective top and bottom sides.
public void turnRowToRight(int row) throws Exception{
int[] mainTopRow = getMain().getRow(row);
getMain().setRow(row, getLeft().getRow(row));
getLeft().setRow(row, getBack().getRow(row));
getBack().setRow(row, getRight().getRow(row));
getRight().setRow(row, mainTopRow);
if(row == 0){
getTop().rotateAntiClockwise();
}else if(row == (getSize() - 1)){
getBottom().rotateClockwise();
}
}
public void turnRowToLeft(int row) throws Exception{
int[] mainTopRow = getMain().getRow(row);
getMain().setRow(row, getRight().getRow(row));
getRight().setRow(row, getBack().getRow(row));
getBack().setRow(row, getLeft().getRow(row));
getLeft().setRow(row, mainTopRow);
if(row == 0){
getTop().rotateClockwise();
}else if(row == (getSize() - 1)){
getBottom().rotateAntiClockwise();
}
}
So now, we need to create the rotation function in the RubikSide
. Rotating clockwise can be visualised as such.
As can be seen from the above diagram, the new rows are just the reversal of each column, so we can create the rotateClockwise
function easily as such.
public void rotateClockwise(){
values = IntStream.range(0, size)
.boxed()
.map(i -> Utils.reverseArray(getCol(i)))
.toArray(int[][]::new);
}
Noticed that we refactor out the reverseArray
function to a utility class to ensure compliance to the SOLID principle.
public class Utils {
public static int[] reverseArray(int[] arr){
return IntStream.rangeClosed(1, arr.length)
.map(i -> arr[arr.length - i])
.toArray();
}
}
To rotate anti-clockwise, our new row 0
is our column n
, and our row n
is our column 0
, as can be visualised as such.
public void rotateAntiClockwise(){
values = IntStream.rangeClosed(1, size)
.boxed()
.map(i -> getCol(size - i))
.toArray(int[][]::new);
}
Turning Column
We want to apply the same logic of turning rows to turning columns. So similarly, to turn a column down, we first save a copy of the values of the column on the main
side. Then we replace the values of the column on the main
side with the values of the column on the top
side, and replace the values of the column on the top
side with the values of the column on the back
side. Continuing on, we replace the values of the columns on the back
side with the values of the column
on the bottom
side, and replace the values of the column on the bottom
side with the values of the column on the main
side which we have saved earlier. And if it is the first or last column, we need to rotate the values on the left
and right
side.
However, there is a litte tricky portion if we want to use this method, because the top part of the back
face is now the bottom, and the left part of the back
is now the right.
We need to flip the back
face, before we can do a simple reassignment of values, so we supplied a reverse2dArray
funtion.
public static int[][] reverse2dArray(int[][] arr){
int[][] interim = IntStream.range(0, arr.length)
.boxed()
.map(i -> reverseArray(arr[i]))
.toArray(int[][]::new);
return IntStream.rangeClosed(1, interim.length)
.boxed()
.map(i -> interim[interim.length - i])
.toArray(int[][]::new);
}
Then we can use this new function to temporary alter the values of the back
face into the perspective we need so that we can do a simple column reassignment. But we need to switch the perspective back as soon as we have finished the column assignment.
public void turnColUp(int col) throws Exception{
int[] mainCol = getMain().getCol(col);
RubikSide reversedBack = getBack().cloneReversed();
getMain().setCol(col, getBottom().getCol(col));
getBottom().setCol(col, reversedBack.getCol(col));
reversedBack.setCol(col, getTop().getCol(col));
getTop().setCol(col, mainCol);
back = reversedBack.cloneReversed();
if(col == 0){
getLeft().rotateAntiClockwise();
}else if(col == (getSize() - 1)){
getRight().rotateClockwise();
}
}
public void turnColDown(int col) throws Exception{
int[] mainCol = getMain().getCol(col);
RubikSide reversedBack = getBack().cloneReversed();
getMain().setCol(col, getTop().getCol(col));
getTop().setCol(col, reversedBack.getCol(col));
reversedBack.setCol(col, getBottom().getCol(col));
getBottom().setCol(col, mainCol);
back = reversedBack.cloneReversed();
if(col == 0){
getLeft().rotateClockwise();
}else if(col == (getSize() - 1)){
getRight().rotateAntiClockwise();
}
}
Rotating main
side
As our goal is to keep things simple and clear, we want to focus all operations just on the main
side. For the rotating of the main
side, it is the same as turning the 1st column of the right
side. But we don’t want to do operations on the other sides, yet we still need to provide all operations possible. Therefore, my solution is to not have a rotate action, but face
action to turn other sides to be the main
side.
Face Action
This should be the easiest action by just replacing the sides. And we only need to provide 5 of such face
actions - face right
, face back
, face left
, face top
, and face bottom
.
public void face(FACE newFace){
Map<FACE, RubikSide> old = Map.of(
FACE.MAIN, getMain().clone(),
FACE.RIGHT, getRight().clone(),
FACE.BACK, getBack().clone(),
FACE.LEFT, getLeft().clone(),
FACE.TOP, getTop().clone(),
FACE.BOTTOM, getBottom().clone()
);
main = getFace(newFace);
right = old.get(getRightFaceOf(newFace));
back = old.get(getBackFaceOf(newFace));
left = old.get(getLeftFaceOf(newFace));
top = old.get(getTopFaceOf(newFace));
bottom = old.get(getBottomFaceOf(newFace));
}
public static FACE getBackFaceOf(FACE face){
if(face == FACE.MAIN) return FACE.BACK;
if(face == FACE.RIGHT) return FACE.LEFT;
if(face == FACE.BACK) return FACE.MAIN;
if(face == FACE.LEFT) return FACE.RIGHT;
if(face == FACE.TOP) return FACE.BOTTOM;
return FACE.TOP;
}
public static FACE getRightFaceOf(FACE face){
if(face == FACE.MAIN) return FACE.RIGHT;
if(face == FACE.RIGHT) return FACE.BACK;
if(face == FACE.BACK) return FACE.LEFT;
if(face == FACE.LEFT) return FACE.MAIN;
return FACE.RIGHT;
}
public static FACE getLeftFaceOf(FACE face){
if(face == FACE.MAIN) return FACE.LEFT;
if(face == FACE.RIGHT) return FACE.MAIN;
if(face == FACE.BACK) return FACE.RIGHT;
if(face == FACE.LEFT) return FACE.BACK;
return FACE.LEFT;
}
public static FACE getTopFaceOf(FACE face){
if(face == FACE.TOP) return FACE.BACK;
if(face == FACE.BOTTOM) return FACE.MAIN;
return FACE.TOP;
}
public static FACE getBottomFaceOf(FACE face){
if(face == FACE.TOP) return FACE.MAIN;
if(face == FACE.BOTTOM) return FACE.BACK;
return FACE.BOTTOM;
}
Defining and Performing Actions
Since the motivation of modelling the rubik’s cube is to solve it, we want a way to easily call any of the actions. A simple way is to just put all possible actions in a list, so that each action is assigned a number. But as our rubik’s cube size is dynamic, the number of possible actions is also dynamic, depending on the number of rows and columns.
Next, we have 2 very different types of actions - Turn
and Face
. But they are both actions, so we generalize them as an interface - RubikCubeAction
.
public interface RubikCubeAction {
String getName();
void performAction(RubikCube rubikCube);
RubikCubeAction oppositeAction();
}
We provide 3 functions here: getName
, performAction
, and oppositeAction
. The getName
is supposed to print the name of the action so that we can visualize what happen. The performAction
utilises a strategy pattern to call the different actions with the same method without a long list of if-else or switch statements. Lastly, in order to make sure our actions are updating the values correctly, we want to run a whole list of actions, then run the reverse list of each action’s opposite action, and it should give us back the perfect rubik’s cube. So we need each action to have an opposite action.
And so we have a FaceAction
and a TurnAction
that implements the RubikCubeAction
. And each of these 2 actions can help to provide all the possible actions for the cube.
public class FaceAction implements RubikCubeAction{
private final RubikCube.FACE face;
private FaceAction(RubikCube.FACE face){
this.face = face;
}
public static FaceAction[] allActions(){
return Arrays.stream(RubikCube.FACE.values())
.filter(face -> face != RubikCube.FACE.MAIN)
.map(FaceAction::new)
.toArray(FaceAction[]::new);
}
@Override
public void performAction(RubikCube rubikCube){
rubikCube.face(face);
}
...
}
public class TurnAction implements RubikCubeAction{
public enum DIRECTION { LEFT, RIGHT, UP, DOWN}
public enum TURN_TYPE { ROW, COL }
public int turnPosition;
public DIRECTION direction;
public TURN_TYPE turnType;
private TurnAction(TURN_TYPE turnType, DIRECTION direction, int turnPosition){
this.turnType = turnType;
this.direction = direction;
this.turnPosition = turnPosition;
}
public static TurnAction[] allActions(int size){
return IntStream.range(0, size)
.boxed()
.flatMap(i -> Stream.of(
new TurnAction(TURN_TYPE.ROW, DIRECTION.LEFT, i),
new TurnAction(TURN_TYPE.ROW, DIRECTION.RIGHT, i),
new TurnAction(TURN_TYPE.COL, DIRECTION.UP, i),
new TurnAction(TURN_TYPE.COL, DIRECTION.DOWN, i)
))
.toArray(TurnAction[]::new);
}
@Override
public void performAction(RubikCube rubikCube){
try {
if(turnType == TURN_TYPE.COL){
if(direction == DIRECTION.UP) rubikCube.turnColUp(turnPosition);
if(direction == DIRECTION.DOWN) rubikCube.turnColDown(turnPosition);
}else if(turnType == TURN_TYPE.ROW){
if(direction == DIRECTION.LEFT) rubikCube.turnRowToLeft(turnPosition);
if(direction == DIRECTION.RIGHT) rubikCube.turnRowToRight(turnPosition);
}
}catch (Exception e){
e.printStackTrace();
}
}
...
}
As of the strategy pattern, we just need a simple call from the RubikCubeAction
interface to execute the action in the RubikCube
.
public void performAction(RubikCubeAction action) {
action.performAction(this);
}
Lastly we can define all the possible actions in the constructor of the RubikCube
.
private RubikCubeAction[] allActions;
public RubikCube(int size){
this.size = size;
main = new RubikSide(size, 1);
right = new RubikSide(size, 2);
back = new RubikSide(size, 3);
left = new RubikSide(size, 4);
top = new RubikSide(size, 5);
bottom = new RubikSide(size, 6);
allActions = Stream.concat(
Stream.of(FaceAction.allActions()),
Stream.of(TurnAction.allActions(size)))
.toArray(RubikCubeAction[]::new);
}
}
Verifying
Testing the Reverse Action List Hypothesis
To ensure what we did so far are correct, we create a RubikSolution
to initialize a 3x3 rubiks cube, and perform a list of random actions, and print out how the values of each side of the cube. Then we run a reverse list of the actions perform and print the cube again. We should get a perfect cube.
In order to make it easier to visualize the outcome of the actions, let’s provide a print method.
public void print(){
String[] box = join(RubikSide.getEmptyString(getSize()), getTop().getString());
Arrays.stream(box).forEach(System.out::println);
box = join(getLeft().getString(), getMain().getString(), getRight().getString(), getBack().getString());
Arrays.stream(box).forEach(System.out::println);
box = join(RubikSide.getEmptyString(getSize()), getBottom().getString());
Arrays.stream(box).forEach(System.out::println);
System.out.println(" ");
}
Random Actions
public List<RubikCubeAction> randomActions(RubikCube rubikCube, int count){
Random random = new Random();
int actionCount = rubikCube.getAllActions().length;
return IntStream.range(0, count).boxed().map(i -> {
RubikCubeAction action = rubikCube.getAllActions()[random.nextInt(actionCount)];
rubikCube.performAction(action);
return action;
}).toList();
}
Reverse actions
public List<RubikCubeAction> reverseActions(List<RubikCubeAction> originalActions){
return IntStream.rangeClosed(1, originalActions.size())
.boxed()
.map(i -> originalActions.get(originalActions.size() - i))
.map(RubikCubeAction::oppositeAction)
.toList();
}
Running the Hypothesis
public static void main(String[] args) {
RubikCube cube = new RubikCube(3);
RubikSolution solution = new RubikSolution();
List<RubikCubeAction> randomActions = solution.randomActions(cube, 20);
randomActions.forEach(action -> System.out.println(action.getName()));
cube.print();
System.out.println(cube.check());
List<RubikCubeAction> reverseActions = solution.reverseActions(randomActions);
reverseActions.forEach(action -> System.out.println(action.getName()));
reverseActions.forEach(cube::performAction);
cube.print();
System.out.println(cube.check());
}
We got a list of actions
TURN_ROW_0_RIGHT
TURN_ROW_1_LEFT
FACE_RIGHT
TURN_COL_2_DOWN
FACE_TOP
FACE_LEFT
TURN_COL_1_UP
TURN_ROW_0_LEFT
TURN_COL_2_UP
FACE_LEFT
FACE_TOP
TURN_ROW_1_LEFT
TURN_COL_1_DOWN
TURN_COL_2_DOWN
TURN_ROW_0_LEFT
FACE_BACK
TURN_COL_2_DOWN
TURN_ROW_0_RIGHT
TURN_COL_2_DOWN
FACE_RIGHT
And print out the rubik’s cube.
[4, 2, 4]
[5, 1, 5]
[3, 4, 2]
[2, 6, 5] [5, 3, 6] [6, 2, 3] [1, 2, 5]
[3, 6, 4] [6, 2, 1] [3, 5, 4] [5, 4, 2]
[4, 1, 1] [3, 5, 6] [1, 1, 3] [5, 3, 1]
[4, 6, 2]
[6, 3, 4]
[6, 1, 2]
Then we got the reversed list of opposite actions
FACE_LEFT
TURN_COL_2_UP
TURN_ROW_0_LEFT
TURN_COL_2_UP
FACE_BACK
TURN_ROW_0_RIGHT
TURN_COL_2_UP
TURN_COL_1_UP
TURN_ROW_1_RIGHT
FACE_BOTTOM
FACE_RIGHT
TURN_COL_2_DOWN
TURN_ROW_0_RIGHT
TURN_COL_1_DOWN
FACE_RIGHT
FACE_BOTTOM
TURN_COL_2_UP
FACE_LEFT
TURN_ROW_1_RIGHT
TURN_ROW_0_LEFT
And we got back our perfect rubik’s cube!
[5, 5, 5]
[5, 5, 5]
[5, 5, 5]
[4, 4, 4] [1, 1, 1] [2, 2, 2] [3, 3, 3]
[4, 4, 4] [1, 1, 1] [2, 2, 2] [3, 3, 3]
[4, 4, 4] [1, 1, 1] [2, 2, 2] [3, 3, 3]
[6, 6, 6]
[6, 6, 6]
[6, 6, 6]
The code for this topic is available on https://github.com/thecodinganalyst/RubiksCube.