I want to know if, given a randomly generated Rubik's Cube face, I can tell if that face corresponds to (at least) one solvable configuration of the Cube. Maybe every random face can be matched to a solvable cube, or maybe not, I'm not sure about that either.
I thought that a good approach would be, for a fixed random face, to build the rest of the cube in a manner that it ends up being solvable. If I can do that, then the face is valid, otherwise it is not.
I would need to implement an algorithm to do that, but I really don't know where to start.
Any suggestions?
Maybe every random face can be matched to a solvable cube
Yes, this is possible. There are even multitudes of remaining possibilities for the faces on the other sides of the cube.
I thought that a good approach would be, for a fixed random face, to build the rest of the cube in a manner that it ends up being solvable.
Indeed. The easiest way to achieve that might be to try to reach the random face configuration starting from a solved cube. As a human would do, perform moves that bring the right colors into place to eventually form the given color pattern of one particular face.
This will always be possible. And by consequence it means you have found a cube state (with that particular face) that can be solved, as the solution consists of reversing the moves you made to get there.
It is not so difficult to bring the right colors into the given face. Start with the center piece of that face. If it is not right, just turn the whole cube until you get it there.
Then focus on each of the edges. It is not so hard to bring edges into place without touching the faces that are already in place.
Finally do the same for corners. It is only slightly harder, as now you need to keep all four edges in place, and any corners that already are well placed.
I thought to have a go at it. So below I implemented a simple Rubik class. An instance represents a cube that by default is in the solved state. One or more moves can be applied to it. Any possible move is really a permutation of 54 stickers, so all possible moves are encoded like that.
Then a function makeTopFace
is added which is specific to this task: it takes the desired pattern as argument, which is an array of 9 values, each representing the sticker color at these positions of the upper face of the cube:
0 1 2
3 4 5
6 7 8
The function will use a Rubik instance to perform moves to bring one sticker after the other into position. At the end the function returns the string of moves that brought the cube into the desired state. No effort was done to make this move list as short as possible, as we are only interested here in the fact whether it is possible.
The main program uses this move list to again generate the cube and display it (flattened out) on the screen, in this format:
(left side) (upper side)
(front side) (right side)
(down side) (back side)
The input can be given interactively by clicking on the stickers of the upper side (which is the second side in the flat representation). One click changes the desired color to another color (round robin). So you can just click these stickers to build your desired configuration. The solution is calculated as you click, so you see an immediate result of a valid cube surrounding the side that you have configured, together with the move list using the notation as on Wikipedia.
So if you take a solved cube in your hands, with the blue side in front of you, the white at the top, and red at the left, and apply the moves as they appear in this application, you'll get the desired configuration on the upper face of your cube.
Here is a runnable snippet, created in JavaScript:
// A simple Rubik class. An instance represents a cube that can mutate by applying moves.
class Rubik {
static init() {
// Define a unique letter for each of the stickers
this.stickers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0abcdefghijklmnopqrstuvwxyz1";
let codes = {
// Define three moves as explicit permutation of 54 stickers:
L: "MGAyEFNHBzKLOIC1QRDTUVWXJZ0abcPefghijklmnopqrstuSYdvwx",
M: "ABCDsFGHIJtLMNOPuRSEUVWXYK0abcdQfghijklmnoTZepqrvwxyz1",
z: "lrxABCkqwGHIjpvMNOdYSPJDeZTQKEf0URLFVWXou1abcntzghimsy",
// Define all other moves as combinations of already defined moves
x: "L'M'z2Lz2",
y: "zxz'",
U: "z'Lz",
F: "yLy'",
R: "z2Lz2",
D: "zLz'",
B: "y'Ly",
E: "zMz'",
S: "yMy'",
l: "LM",
u: "UE'",
f: "FS",
r: "RM'",
d: "DE",
b: "BS'",
};
// Register the above moves, together with their doubles and opposites:
for (let [move, code] of Object.entries(codes)) {
this[move] = code.length === 54 ? this.fromCode(code) : new Rubik().apply(code);
this[move+"2"] = this[move].clone().apply(this[move]);
this[move+"'"] = this[move+"2"].clone().apply(this[move]);
}
this.stickerColor = `
LLLUUU
LLLUUU
LLLUUU
FFFRRR
FFFRRR
FFFRRR
DDDBBB
DDDBBB
DDDBBB`.match(/\S/g);
}
static fromCode(code) {
return new Rubik(Array.from(code, c => Rubik.stickers.indexOf(c)))
}
constructor(permutation=Array(54).keys()) { // Optional argument: a permutation array with 54 entries
if (!Rubik.stickers) Rubik.init();
// We identify 54 stickers and 54 possible locations with the same numbering:
this.state = [...permutation];
// Index = address at fixed location in space.
// Value = sticker, i.e. the home-address of the sticker at this address.
this.log = ""; // Keep track of moves that have been applied to this cube
}
clone() {
return Object.assign(new Rubik, { state: [...this.state] });
}
apply(other) {
if (typeof other === "string") {
this.log += other;
for (let move of other.match(/\w['2]?/g) || []) {
this.apply(Rubik[move]);
}
} else {
this.state = other.state.map(orig => this.state[orig]);
}
return this;
}
getLog() {
// Remove the most obvious cases of neutralising moves
return this.log.replace(/(\w)'/g, "$1$1$1")
.replace(/(\w)2/g, "$1$1")
.replace(/(\w)\1{3}/g, "") // removal
.replace(/(\w)\1{2}/g, "$1'")
.replace(/(\w)\1/g, "$12");
}
toCode() {
return this.state.map(i => Rubik.stickers[i]).join("");
}
toString() {
return this.state.map((sticker, i) =>
(i%6 ? "" : "\n".padEnd(1 + Math.floor(i / 18) * 3)) + Rubik.stickerColor[sticker]
).join("").slice(1);
}
toHtml() {
return "<table><tr>"
+ this.toString().replace(/./gs, m => m === "\n" ? '</tr><tr>' : `<td class="${m}"><\/td>`)
+ "</tr></table>";
}
}
// Specific function for this question.
// Takes an array with 9 "colors" (from "LUFRDB"), which represents a target
// configuration of the upper side.
// This function will return moves that turn a solved cube into a cube
// that has the upper side look like the given pattern.
function makeTopSide(topSide) {
let cube = new Rubik;
// Center:
let k = [7, 25, 28, 43, 46].map(i => Rubik.stickerColor[cube.state[i]]).indexOf(topSide[4]);
if (k > -1) { // Need to turn the cube to bring the right center piece into position
cube.apply(["z", "x", "z'", "z2", "x'"][k]);
}
// Edges:
for (let j of [7, 5, 1, 3]) { // For each edge
let color = topSide[j]; // Get its desired color
if (Rubik.stickerColor[cube.state[16]] !== color) {
for (let i = 0; i < 4; i++) {
let k = [13, 42, 27, 34].map(i => Rubik.stickerColor[cube.state[i]]).indexOf(color);
if (k > -1) {
cube.apply(["F", "F2", "F'", "RF'R'"][k]);
break;
}
cube.apply("d");
}
if (Rubik.stickerColor[cube.state[19]] === color) {
cube.apply("Fd'F");
}
}
cube.apply("y"); // Turn the next edge into view
}
// Corners:
for (let j of [8, 2, 0, 6]) { // For each corner:
let color = topSide[j]; // Get its desired color
if (Rubik.stickerColor[cube.state[17]] !== color) {
for (let i = 0; i < 4; i++) {
let k = [32, 33, 36].map(i => Rubik.stickerColor[cube.state[i]]).indexOf(color);
if (k > -1) {
cube.apply(["FDF'", "R'D'R", "R'DRFD2F'"][k]);
break;
}
cube.apply("D");
}
let k = cube.state.slice(20, 22).map(st => Rubik.stickerColor[st]).indexOf(color);
if (k > -1) {
cube.apply(["R'DRFDF'", "FD'F'R'D'R"][k]);
}
}
cube.apply("y"); // Turn the next corner into view
}
return cube.getLog();
}
// I/O handling
let output = document.getElementById("output");
let log = document.getElementById("log");
let topSide = [..."UUUUUUUUU"];
function display(cube) {
output.innerHTML = cube.toHtml();
log.textContent = cube.getLog();
}
display(new Rubik);
function changeSticker(i) {
// Change the color of the clicked sticker
topSide[i] = "UFRDBL"["LUFRDB".indexOf(topSide[i])];
// Find out which moves generate a cube that has this upper side pattern
let moves = makeTopSide(topSide);
let cube = new Rubik().apply(moves);
display(cube);
}
output.addEventListener("click", function (e) {
let td = e.target;
// Only allow changing stickers on the upper side of the cube
if (td.tagName !== "TD" || td.cellIndex < 3 || td.parentNode.rowIndex > 2) return;
changeSticker(td.cellIndex - 3 + td.parentNode.rowIndex * 3);
});
#output td { width: 15px; height: 15px }
#output .L { background: red }
#output .U { background: #eee }
#output .F { background: blue }
#output .R { background: orange }
#output .D { background: yellow }
#output .B { background: green }
#output tr:first-child td:nth-child(4),
#output tr:first-child td:nth-child(5),
#output tr:first-child td:nth-child(6),
#output tr:nth-child(2) td:nth-child(4),
#output tr:nth-child(2) td:nth-child(5),
#output tr:nth-child(2) td:nth-child(6),
#output tr:nth-child(3) td:nth-child(4),
#output tr:nth-child(3) td:nth-child(5),
#output tr:nth-child(3) td:nth-child(6) { cursor: pointer }
<div id="output"></div>
<div id="log"></div>