Spaces:
Running
Running
File size: 6,687 Bytes
89c2146 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | "use strict";
/*
* Rubik's Cube Solver worker.
*
* The vendored cube.js and min2phase.js files are used under their MIT
* licenses; see the license files in vendor/.
* Keeping search in a Web Worker prevents its one-time table generation and
* each solve from blocking the 3D controls or move player.
*/
const workerSignature = new URLSearchParams(self.location.search).get("__sign");
function signedAssetUrl(path) {
const url = new URL(path, self.location.href);
if (workerSignature) url.searchParams.set("__sign", workerSignature);
return url.href;
}
self.postMessage({type: "status", status: "loading", message: "Loading cube parser…"});
importScripts(signedAssetUrl("./vendor/cube.js"));
self.postMessage({type: "status", status: "loading", message: "Loading fast search engine…"});
importScripts(signedAssetUrl("./vendor/min2phase.js"));
const Min2Phase = self.min2phase;
self.postMessage({type: "status", status: "loaded", message: "Initializing solve tables…"});
const FACE_ORDER = ["U", "R", "F", "D", "L", "B"];
const SOLVED = FACE_ORDER.map(face => face.repeat(9)).join("");
let workerInitialized = false;
function fail(code, message) {
const error = new Error(message);
error.code = code;
throw error;
}
function isPermutation(values, size) {
return Array.isArray(values)
&& values.length === size
&& values.every(Number.isInteger)
&& new Set(values).size === size
&& values.every(value => value >= 0 && value < size);
}
function permutationParity(values) {
let inversions = 0;
for (let left = 0; left < values.length; left++) {
for (let right = left + 1; right < values.length; right++) {
if (values[left] > values[right]) inversions++;
}
}
return inversions % 2;
}
function normalizeCube(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
fail("input_type", "Cube input must contain the six U/R/F/D/L/B faces.");
}
const values = [];
for (const face of FACE_ORDER) {
const facelets = input[face];
if (!Array.isArray(facelets) || facelets.flat(Infinity).length !== 9) {
fail("face_shape", `The ${face} face must contain exactly nine stickers.`);
}
values.push(...facelets.flat(Infinity));
}
if (values.some(value => typeof value !== "string" || !value)) {
fail("empty_sticker", "Every sticker needs one of the six chosen colors.");
}
const centers = FACE_ORDER.map((_, index) => values[index * 9 + 4]);
if (new Set(centers).size !== 6) {
fail("duplicate_centers", "Each face center must use a different color.");
}
const counts = new Map();
values.forEach(value => counts.set(value, (counts.get(value) || 0) + 1));
if (counts.size !== 6 || [...counts.values()].some(count => count !== 9)) {
fail("color_multiplicity", "Every cube color must appear exactly nine times.");
}
const colorToFace = new Map(centers.map((color, index) => [color, FACE_ORDER[index]]));
const canonical = values.map(value => colorToFace.get(value) || "?").join("");
if (canonical.includes("?")) {
fail("unknown_color", "Every sticker must match one of the six center colors.");
}
const cube = Cube.fromString(canonical);
const state = cube.toJSON();
// fromString is intentionally permissive, so require an exact round trip
// and then verify all physical invariants ourselves before search.
if (cube.asString() !== canonical || !isPermutation(state.cp, 8) || !isPermutation(state.ep, 12)) {
fail("piece_set", "These stickers do not form the eight real corners and twelve real edges. Recheck the colors on each piece.");
}
if (!Array.isArray(state.co) || state.co.length !== 8 || state.co.some(value => !Number.isInteger(value) || value < 0 || value > 2)) {
fail("corner_orientation", "A corner has an impossible orientation. Recheck the three colors around each corner.");
}
if (!Array.isArray(state.eo) || state.eo.length !== 12 || state.eo.some(value => value !== 0 && value !== 1)) {
fail("edge_orientation", "An edge has an impossible orientation. Recheck the two colors on each edge.");
}
if (state.co.reduce((sum, value) => sum + value, 0) % 3 !== 0) {
fail("corner_twist", "This state has one or more impossible twisted corners. Recheck the corner stickers.");
}
if (state.eo.reduce((sum, value) => sum + value, 0) % 2 !== 0) {
fail("edge_flip", "This state has an impossible flipped edge. Recheck the edge stickers.");
}
if (permutationParity(state.cp) !== permutationParity(state.ep)) {
fail("parity", "This state has an impossible piece swap. Recheck the stickers on the last pieces you entered.");
}
return {
canonical,
faceToColor: Object.fromEntries(FACE_ORDER.map((face, index) => [face, centers[index]])),
cube,
};
}
function parseMoves(algorithm) {
if (!algorithm || !algorithm.trim()) return [];
const moves = algorithm.trim().split(/\s+/);
if (moves.some(move => !/^[URFDLB](?:2|')?$/.test(move))) {
fail("solver_output", "The solver returned unsupported move notation.");
}
return moves;
}
function initialize() {
if (workerInitialized) return;
self.postMessage({type: "status", status: "preparing"});
Min2Phase.initFull();
workerInitialized = true;
self.postMessage({type: "status", status: "ready"});
}
self.onmessage = event => {
const message = event.data || {};
if (message.type === "init") {
try {
initialize();
} catch (error) {
self.postMessage({
type: "status",
status: "error",
message: `The browser solver could not initialize${error && error.message ? `: ${error.message}` : "."}`,
});
}
return;
}
if (message.type !== "solve") return;
try {
initialize();
const normalized = normalizeCube(message.cube);
const started = performance.now();
const algorithm = normalized.canonical === SOLVED ? "" : Min2Phase.solve(normalized.canonical);
const searchSeconds = (performance.now() - started) / 1000;
if (algorithm === null || /^Error\s/i.test(algorithm)) {
fail("search_depth", "No solution was found within 22 moves. Please recheck the entered stickers.");
}
self.postMessage({
type: "result",
id: message.id,
ok: true,
canonical: normalized.canonical,
faceToColor: normalized.faceToColor,
moves: parseMoves(algorithm),
searchSeconds,
});
} catch (error) {
self.postMessage({
type: "result",
id: message.id,
ok: false,
error: {
code: error && error.code ? error.code : "solver_failure",
message: error && error.message ? error.message : "The cube could not be solved.",
},
});
}
};
|