fix(solver): replace exponential IDDFS recursion with instantaneous heuristic simulation macros
This commit is contained in:
@@ -7,6 +7,7 @@ export class BeginnerSolver {
|
||||
}
|
||||
|
||||
apply(moveStr) {
|
||||
if (!moveStr) return;
|
||||
const moveArr = moveStr.split(" ").filter((m) => m);
|
||||
for (const m of moveArr) {
|
||||
if (!MOVES[m]) throw new Error(`Invalid move: ${m}`);
|
||||
@@ -17,8 +18,6 @@ export class BeginnerSolver {
|
||||
|
||||
solve() {
|
||||
this.solution = [];
|
||||
|
||||
// Safety check - is it already solved?
|
||||
if (this.isSolvedState(this.cube)) return [];
|
||||
|
||||
console.log("Starting Cross");
|
||||
@@ -34,11 +33,8 @@ export class BeginnerSolver {
|
||||
console.log("Starting Yellow PLL");
|
||||
this.permuteYellowCorners();
|
||||
this.permuteYellowEdges();
|
||||
|
||||
// Optional: align U face
|
||||
this.alignUFace();
|
||||
|
||||
// Collapse redundant moves like U U -> U2, R R' -> nothing
|
||||
this.optimizeSolution();
|
||||
return this.solution;
|
||||
}
|
||||
@@ -51,210 +47,216 @@ export class BeginnerSolver {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generalized Depth-Limited Search for localized piece-by-piece goals
|
||||
solveGoal(condition, maxDepth, allowedMoves = Object.keys(MOVES)) {
|
||||
if (condition(this.cube)) return true;
|
||||
|
||||
const opposing = { U: "D", D: "U", L: "R", R: "L", F: "B", B: "F" };
|
||||
|
||||
const dfs = (node, depth, lastMove) => {
|
||||
if (depth === 0) return condition(node) ? [] : null;
|
||||
const lastFace = lastMove ? lastMove[0] : null;
|
||||
|
||||
for (let m of allowedMoves) {
|
||||
const face = m[0];
|
||||
if (face === lastFace) continue;
|
||||
if (opposing[face] === lastFace && face > lastFace) continue;
|
||||
|
||||
const nextNode = node.multiply(MOVES[m]);
|
||||
if (depth === 1) {
|
||||
if (condition(nextNode)) return [m];
|
||||
} else {
|
||||
const path = dfs(nextNode, depth - 1, m);
|
||||
if (path) return [m, ...path];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (let d = 1; d <= maxDepth; d++) {
|
||||
const res = dfs(this.cube, d, "");
|
||||
if (res) {
|
||||
res.forEach((m) => this.apply(m));
|
||||
return true;
|
||||
}
|
||||
testAlg(algStr, targetId, isCorner) {
|
||||
let temp = this.cube;
|
||||
const arr = algStr.split(" ").filter((m) => m);
|
||||
for (const m of arr) temp = temp.multiply(MOVES[m]);
|
||||
if (isCorner) {
|
||||
return temp.cp[targetId] === targetId && temp.co[targetId] === 0;
|
||||
} else {
|
||||
return temp.ep[targetId] === targetId && temp.eo[targetId] === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
solveCross() {
|
||||
// White Cross on D face
|
||||
// Edge Pieces: DF(5), DR(4), DB(7), DL(6)
|
||||
const targets = [5, 4, 7, 6];
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
const goal = (state) => {
|
||||
for (let j = 0; j <= i; j++) {
|
||||
const t = targets[j];
|
||||
if (state.ep[t] !== t || state.eo[t] !== 0) return false;
|
||||
const targets = [
|
||||
{ id: 5, up: 1, ins: ["F2", "U' R' F R", "U L F' L'"] }, // DF
|
||||
{ id: 4, up: 0, ins: ["R2", "U' B' R B", "U F R' F'"] }, // DR
|
||||
{ id: 7, up: 3, ins: ["B2", "U' L' B L", "U R B' R'"] }, // DB
|
||||
{ id: 6, up: 2, ins: ["L2", "U' F' L F", "U B L' B'"] }, // DL
|
||||
];
|
||||
|
||||
for (let t of targets) {
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 15) {
|
||||
let pos = this.cube.ep.indexOf(t.id);
|
||||
if (pos === t.id && this.cube.eo[pos] === 0) break;
|
||||
|
||||
if ([4, 5, 6, 7].includes(pos)) {
|
||||
if (pos === 5) this.apply("F2");
|
||||
else if (pos === 4) this.apply("R2");
|
||||
else if (pos === 7) this.apply("B2");
|
||||
else if (pos === 6) this.apply("L2");
|
||||
} else if ([8, 9, 10, 11].includes(pos)) {
|
||||
if (pos === 8) this.apply("R U R'");
|
||||
else if (pos === 9) this.apply("F U F'");
|
||||
else if (pos === 10) this.apply("L U L'");
|
||||
else if (pos === 11) this.apply("B U B'");
|
||||
} else if ([0, 1, 2, 3].includes(pos)) {
|
||||
let success = false;
|
||||
for (let u = 0; u < 4; u++) {
|
||||
for (let alg of t.ins) {
|
||||
if (this.testAlg(alg, t.id, false)) {
|
||||
this.apply(alg);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) break;
|
||||
this.apply("U");
|
||||
}
|
||||
if (success) break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!this.solveGoal(goal, 7)) throw new Error("Failed Cross depth 7");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
solveF2LCorners() {
|
||||
// DFR(4), DRB(7), DBL(6), DLF(5)
|
||||
const corners = [4, 7, 6, 5];
|
||||
for (let i = 0; i < corners.length; i++) {
|
||||
const goal = (state) => {
|
||||
// Must preserve cross
|
||||
if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
|
||||
if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
|
||||
if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
|
||||
if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
|
||||
const targets = [
|
||||
{ id: 4, ext: "R U R'", ins: ["R U2 R' U' R U R'", "R U R'", "F' U' F"] },
|
||||
{ id: 5, ext: "F U F'", ins: ["F U2 F' U' F U F'", "F U F'", "L' U' L"] },
|
||||
{ id: 6, ext: "L U L'", ins: ["L U2 L' U' L U L'", "L U L'", "B' U' B"] },
|
||||
{ id: 7, ext: "B U B'", ins: ["B U2 B' U' B U B'", "B U B'", "R' U' R"] },
|
||||
];
|
||||
|
||||
// Must preserve prior corners
|
||||
for (let j = 0; j <= i; j++) {
|
||||
const c = corners[j];
|
||||
if (state.cp[c] !== c || state.co[c] !== 0) return false;
|
||||
for (let t of targets) {
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 15) {
|
||||
let pos = this.cube.cp.indexOf(t.id);
|
||||
if (pos === t.id && this.cube.co[pos] === 0) break;
|
||||
|
||||
if ([4, 5, 6, 7].includes(pos)) {
|
||||
if (pos === 4) this.apply("R U R'");
|
||||
else if (pos === 5) this.apply("F U F'");
|
||||
else if (pos === 6) this.apply("L U L'");
|
||||
else if (pos === 7) this.apply("B U B'");
|
||||
} else if ([0, 1, 2, 3].includes(pos)) {
|
||||
let success = false;
|
||||
for (let u = 0; u < 4; u++) {
|
||||
for (let alg of t.ins) {
|
||||
if (this.testAlg(alg, t.id, true)) {
|
||||
this.apply(alg);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) break;
|
||||
this.apply("U");
|
||||
}
|
||||
if (success) break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!this.solveGoal(goal, 8))
|
||||
throw new Error("Failed F2L Corners depth 8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
solveF2LEdges() {
|
||||
// FR(8), BR(11), BL(10), FL(9)
|
||||
const edges = [8, 11, 10, 9];
|
||||
const allowed = [
|
||||
"U",
|
||||
"U'",
|
||||
"U2",
|
||||
"R",
|
||||
"R'",
|
||||
"R2",
|
||||
"F",
|
||||
"F'",
|
||||
"F2",
|
||||
"L",
|
||||
"L'",
|
||||
"L2",
|
||||
"B",
|
||||
"B'",
|
||||
"B2",
|
||||
]; // Avoid D moves
|
||||
const targets = [
|
||||
{
|
||||
id: 8,
|
||||
ext: "R U R'",
|
||||
ins: ["U R U' R' U' F' U F", "U' F' U F U R U' R'"],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
ext: "F U F'",
|
||||
ins: ["U' L' U L U F U' F'", "U F U' F' U' L' U L"],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
ext: "L U L'",
|
||||
ins: ["U L U' L' U' B' U B", "U' B' U B U L U' L'"],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
ext: "B U B'",
|
||||
ins: ["U B U' B' U' R' U R", "U' R' U R U B U' B'"],
|
||||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
const goal = (state) => {
|
||||
// Preserve cross
|
||||
if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
|
||||
if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
|
||||
if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
|
||||
if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
|
||||
// Preserve all D corners
|
||||
if (state.cp[4] !== 4 || state.co[4] !== 0) return false;
|
||||
if (state.cp[7] !== 7 || state.co[7] !== 0) return false;
|
||||
if (state.cp[6] !== 6 || state.co[6] !== 0) return false;
|
||||
if (state.cp[5] !== 5 || state.co[5] !== 0) return false;
|
||||
for (let t of targets) {
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 15) {
|
||||
let pos = this.cube.ep.indexOf(t.id);
|
||||
if (pos === t.id && this.cube.eo[pos] === 0) break;
|
||||
|
||||
// Preserve prior edges
|
||||
for (let j = 0; j <= i; j++) {
|
||||
const e = edges[j];
|
||||
if (state.ep[e] !== e || state.eo[e] !== 0) return false;
|
||||
if ([8, 9, 10, 11].includes(pos)) {
|
||||
if (pos === 8)
|
||||
this.apply("R U R' U' F' U' F"); // Extract standard way
|
||||
else if (pos === 9) this.apply("F U F' U' L' U' L");
|
||||
else if (pos === 10) this.apply("L U L' U' B' U' B");
|
||||
else if (pos === 11) this.apply("B U B' U' R' U' R");
|
||||
} else if ([0, 1, 2, 3].includes(pos)) {
|
||||
let success = false;
|
||||
for (let u = 0; u < 4; u++) {
|
||||
for (let alg of t.ins) {
|
||||
if (this.testAlg(alg, t.id, false)) {
|
||||
this.apply(alg);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) break;
|
||||
this.apply("U");
|
||||
}
|
||||
if (success) break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// Depth 10 is needed for inserting edge from bad positions
|
||||
if (!this.solveGoal(goal, 10, allowed))
|
||||
throw new Error("Failed F2L Edges depth 10");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- BEGIN CFOP LAST LAYER MACROS ---
|
||||
solveYellowCross() {
|
||||
// Find yellow cross edges: UR(0), UF(1), UL(2), UB(3)
|
||||
// They just need orientation eo=0.
|
||||
const getOrientedCount = () =>
|
||||
[0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0).length;
|
||||
|
||||
let safetyCount = 0;
|
||||
while (getOrientedCount() < 4 && safetyCount++ < 10) {
|
||||
const oriented = [0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0);
|
||||
|
||||
if (oriented.length === 0) {
|
||||
this.apply("F R U R' U' F'");
|
||||
} else if (oriented.length === 2) {
|
||||
// Line (opposite) or L-shape (adjacent)
|
||||
const [a, b] = oriented;
|
||||
if (Math.abs(a - b) === 2) {
|
||||
// Line geometry (UR and UL, or UF and UB)
|
||||
// To apply F R U R' U' F', the line must be horizontal.
|
||||
// If line is UR(0) and UL(2), it's horizontal from F perspective.
|
||||
if (a === 0 && b === 2) {
|
||||
this.apply("F R U R' U' F'");
|
||||
} else {
|
||||
this.apply("U"); // turn line so it is UR/UL
|
||||
this.apply("F R U R' U' F'");
|
||||
}
|
||||
} else {
|
||||
// L-shape geometry
|
||||
// Macro: F U R U' R' F' requires L to be at Back and Left (UB=3, UL=2)
|
||||
if (oriented.includes(3) && oriented.includes(2)) {
|
||||
this.apply("F U R U' R' F'");
|
||||
} else {
|
||||
if (Math.abs(a - b) === 2 || (a === 0 && b === 3)) {
|
||||
// Line or L-shape handling simplified
|
||||
let succ = false;
|
||||
for (let u = 0; u < 4; u++) {
|
||||
let tmp = this.cube.clone();
|
||||
let p1 = (temp) => {
|
||||
let c = temp.clone();
|
||||
"F R U R' U' F'"
|
||||
.split(" ")
|
||||
.filter((x) => x)
|
||||
.forEach((m) => (c = c.multiply(MOVES[m])));
|
||||
return c;
|
||||
};
|
||||
let p2 = (temp) => {
|
||||
let c = temp.clone();
|
||||
"F U R U' R' F'"
|
||||
.split(" ")
|
||||
.filter((x) => x)
|
||||
.forEach((m) => (c = c.multiply(MOVES[m])));
|
||||
return c;
|
||||
};
|
||||
if ([0, 1, 2, 3].filter((i) => p1(tmp).eo[i] === 0).length === 4) {
|
||||
this.apply("F R U R' U' F'");
|
||||
succ = true;
|
||||
break;
|
||||
}
|
||||
if ([0, 1, 2, 3].filter((i) => p2(tmp).eo[i] === 0).length === 4) {
|
||||
this.apply("F U R U' R' F'");
|
||||
succ = true;
|
||||
break;
|
||||
}
|
||||
this.apply("U");
|
||||
}
|
||||
if (!succ) this.apply("F R U R' U' F'"); // fallback
|
||||
} else {
|
||||
this.apply("U");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orientYellowCorners() {
|
||||
// Sune / Antisune to orient yellow corners (co=0)
|
||||
// OLL for corners alone using the repetitive beginner algorithm (R' D' R D)
|
||||
// Position the unsolved corner at URF (0) and repeat R' D' R D until co[0] === 0
|
||||
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 20) {
|
||||
let solvedCount = [0, 1, 2, 3].filter(
|
||||
(i) => this.cube.co[i] === 0,
|
||||
).length;
|
||||
if (solvedCount === 4) break;
|
||||
|
||||
if (this.cube.co[0] === 0) {
|
||||
this.apply("U"); // Next
|
||||
} else {
|
||||
// Apply R' D' R D twice (1 cycle)
|
||||
this.apply("R' D' R D R' D' R D");
|
||||
}
|
||||
while (safetyCount++ < 25) {
|
||||
if ([0, 1, 2, 3].filter((i) => this.cube.co[i] === 0).length === 4) break;
|
||||
if (this.cube.co[0] === 0) this.apply("U");
|
||||
else this.apply("R' D' R D R' D' R D");
|
||||
}
|
||||
}
|
||||
|
||||
permuteYellowCorners() {
|
||||
// Beginner method: look for "headlights" (two corners on the same face with the same color targeting that face).
|
||||
// Mathematically: two adjacent U corners whose permutation matches their distance.
|
||||
// E.g., URF(0) and UBR(3). If their permuted values are also adjacent, they are headlights.
|
||||
|
||||
const hasHeadlightsOnBack = () => {
|
||||
// Back corners are ULB(2) and UBR(3)
|
||||
// To be headlights on the back, they must belong to the same face in their solved state.
|
||||
// Wait, comparing corner colors mathematically:
|
||||
// In a solved cube, the U face has 4 corners: 0, 1, 2, 3.
|
||||
// The distance between cp[2] and cp[3] modulo 4 should be exactly 1 or 3 (adjacent).
|
||||
// Actually, if they are correctly relative to EACH OTHER:
|
||||
return (
|
||||
(this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 3 ||
|
||||
(this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1
|
||||
);
|
||||
};
|
||||
|
||||
// Simplified: Just keep applying the A-perm (headlight creator) until all 4 corners are relatively solved.
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 10) {
|
||||
// Check if corners are relatively solved (i.e. 0->1->2->3 in order)
|
||||
while (safetyCount++ < 15) {
|
||||
let c0 = this.cube.cp[0],
|
||||
c1 = this.cube.cp[1],
|
||||
c2 = this.cube.cp[2],
|
||||
@@ -263,47 +265,43 @@ export class BeginnerSolver {
|
||||
(c1 - c0 + 4) % 4 === 1 &&
|
||||
(c2 - c1 + 4) % 4 === 1 &&
|
||||
(c3 - c2 + 4) % 4 === 1
|
||||
) {
|
||||
break; // All corners are cyclically ordered
|
||||
}
|
||||
)
|
||||
break;
|
||||
|
||||
// If Back corners are cyclically ordered (Headlights on Back)
|
||||
if ((c2 - c3 + 4) % 4 === 1 || (c3 - c2 + 4) % 4 === 3) {
|
||||
// Wait, order must be 3->0->1->2. So ULB(2) should be next after UBR(3).
|
||||
// If cp[2] comes immediately after cp[3] cyclically:
|
||||
if ((this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1) {
|
||||
this.apply("R' F R' B2 R F' R' B2 R2");
|
||||
} else {
|
||||
this.apply("U");
|
||||
let succ = false;
|
||||
for (let u = 0; u < 4; u++) {
|
||||
for (let alg of [
|
||||
"R' F R' B2 R F' R' B2 R2",
|
||||
"R B' R F2 R' B R F2 R2",
|
||||
]) {
|
||||
let t = this.cube.clone();
|
||||
alg.split(" ").forEach((m) => (t = t.multiply(MOVES[m])));
|
||||
let tc0 = t.cp[0],
|
||||
tc1 = t.cp[1],
|
||||
tc2 = t.cp[2],
|
||||
tc3 = t.cp[3];
|
||||
if (
|
||||
(tc1 - tc0 + 4) % 4 === 1 &&
|
||||
(tc2 - tc1 + 4) % 4 === 1 &&
|
||||
(tc3 - tc2 + 4) % 4 === 1
|
||||
) {
|
||||
this.apply(alg);
|
||||
succ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find ANY headlights and put them on the back
|
||||
if ((c1 - c2 + 4) % 4 === 1) {
|
||||
this.apply("U");
|
||||
continue;
|
||||
}
|
||||
if ((c0 - c1 + 4) % 4 === 1) {
|
||||
this.apply("U2");
|
||||
continue;
|
||||
}
|
||||
if ((c3 - c0 + 4) % 4 === 1) {
|
||||
this.apply("U'");
|
||||
continue;
|
||||
}
|
||||
|
||||
// No headlights at all (diagonal swap), just apply the alg anywhere
|
||||
this.apply("R' F R' B2 R F' R' B2 R2");
|
||||
if (succ) break;
|
||||
this.apply("U");
|
||||
}
|
||||
if (succ) break;
|
||||
this.apply("R' F R' B2 R F' R' B2 R2");
|
||||
}
|
||||
}
|
||||
|
||||
permuteYellowEdges() {
|
||||
// Corners are properly ordered now. We just need to align them to the centers first.
|
||||
while (this.cube.cp[0] !== 0) {
|
||||
this.apply("U");
|
||||
}
|
||||
let s = 0;
|
||||
while (this.cube.cp[0] !== 0 && s++ < 5) this.apply("U");
|
||||
|
||||
// Now corners are 100% solved. Check edges.
|
||||
let safetyCount = 0;
|
||||
while (safetyCount++ < 10) {
|
||||
if (
|
||||
@@ -311,62 +309,62 @@ export class BeginnerSolver {
|
||||
this.cube.ep[1] === 1 &&
|
||||
this.cube.ep[2] === 2 &&
|
||||
this.cube.ep[3] === 3
|
||||
) {
|
||||
break; // fully solved!
|
||||
}
|
||||
)
|
||||
break;
|
||||
|
||||
// Check if one edge is solved. If so, put it at the BACK (UB=3)
|
||||
if (this.cube.ep[3] === 3) {
|
||||
// Apply U-perm
|
||||
this.apply("R U' R U R U R U' R' U' R2");
|
||||
} else if (this.cube.ep[0] === 0) {
|
||||
this.apply("U"); // turn whole cube or just U... wait, standard alg rotates U.
|
||||
// To keep corners solved but move edges, we'd have to do y rotations.
|
||||
// Let's just use the U-perm and see if it solves.
|
||||
// If UR(0) is solved, we want it at UB(3). So we do a U move, apply alg, do U'.
|
||||
// Wait, applying U moves the corners! We can't do that.
|
||||
// Standard solving often uses `y` cube rotations. Since our model only supports face moves,
|
||||
// we can use the transposed algorithms.
|
||||
// Alg for solved Right(0): F U' F U F U F U' F' U' F2
|
||||
this.apply("F U' F U F U F U' F' U' F2");
|
||||
} else if (this.cube.ep[1] === 1) {
|
||||
this.apply("L U' L U L U L U' L' U' L2");
|
||||
} else if (this.cube.ep[2] === 2) {
|
||||
this.apply("B U' B U B U B U' B' U' B2");
|
||||
} else {
|
||||
// No edges solved. Apply U-perm from anywhere.
|
||||
this.apply("R U' R U R U R U' R' U' R2");
|
||||
let succ = false;
|
||||
const uMoves = ["", "U ", "U2 ", "U' "];
|
||||
const uMovesInv = ["", "U' ", "U2 ", "U "];
|
||||
|
||||
for (let u = 0; u < 4; u++) {
|
||||
for (let baseAlg of [
|
||||
"R U' R U R U R U' R' U' R2",
|
||||
"L' U L' U' L' U' L' U L U L2",
|
||||
]) {
|
||||
const fullAlg = uMoves[u] + baseAlg + " " + uMovesInv[u];
|
||||
let t = this.cube.clone();
|
||||
fullAlg
|
||||
.split(" ")
|
||||
.filter((x) => x)
|
||||
.forEach((m) => (t = t.multiply(MOVES[m])));
|
||||
if (
|
||||
t.ep[0] === 0 &&
|
||||
t.ep[1] === 1 &&
|
||||
t.ep[2] === 2 &&
|
||||
t.ep[3] === 3
|
||||
) {
|
||||
this.apply(fullAlg);
|
||||
succ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (succ) break;
|
||||
}
|
||||
if (succ) break;
|
||||
this.apply("R U' R U R U R U' R' U' R2"); // Fallback cycle
|
||||
}
|
||||
}
|
||||
|
||||
alignUFace() {
|
||||
while (this.cube.cp[0] !== 0) {
|
||||
this.apply("U");
|
||||
}
|
||||
let s = 0;
|
||||
while (this.cube.cp[0] !== 0 && s++ < 5) this.apply("U");
|
||||
}
|
||||
|
||||
optimizeSolution() {
|
||||
// A quick pass to cancel out redundant moves like U U' -> nothing, U U -> U2, U2 U -> U'
|
||||
let stable = false;
|
||||
while (!stable) {
|
||||
stable = true;
|
||||
for (let i = 0; i < this.solution.length - 1; i++) {
|
||||
const a = this.solution[i];
|
||||
const b = this.solution[i + 1];
|
||||
|
||||
if (a[0] === b[0]) {
|
||||
// Same face! Let's sum the rotation.
|
||||
const val = (m) => (m.includes("'") ? -1 : m.includes("2") ? 2 : 1);
|
||||
let sum = (val(a) + val(b)) % 4;
|
||||
if (sum < 0) sum += 4; // normalize to positive
|
||||
|
||||
this.solution.splice(i, 2); // remove both
|
||||
|
||||
if (sum < 0) sum += 4;
|
||||
this.solution.splice(i, 2);
|
||||
if (sum === 1) this.solution.splice(i, 0, a[0]);
|
||||
else if (sum === 2) this.solution.splice(i, 0, a[0] + "2");
|
||||
else if (sum === 3) this.solution.splice(i, 0, a[0] + "'");
|
||||
|
||||
stable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
9
test/test_aperm.js
Normal file
9
test/test_aperm.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
let cube = new DeepCube();
|
||||
const apply = (str) => { str.split(' ').filter(x => x).forEach(m => cube = cube.multiply(MOVES[m])); };
|
||||
|
||||
apply("R' F R' B2 R F' R' B2 R2");
|
||||
console.log(`cp after A-perm:`, cube.cp.slice(0, 4));
|
||||
// We want to see which two corners are swapped.
|
||||
// Solved is 0,1,2,3.
|
||||
// If it prints 0,1,3,2, then 2 and 3 are swapped (Back corners).
|
||||
25
test/test_beginner_freeze.js
Normal file
25
test/test_beginner_freeze.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
|
||||
|
||||
let cube = new DeepCube();
|
||||
const scramble = "R U R' U' R' F R2 U' R' U' R U R' F'"; // T-perm
|
||||
scramble.split(' ').forEach(move => {
|
||||
cube = cube.multiply(MOVES[move]);
|
||||
});
|
||||
|
||||
console.log('Testing BeginnerSolver with T-perm...');
|
||||
const solver = new BeginnerSolver(cube);
|
||||
|
||||
// Add some logging to the solver's methods to trace execution
|
||||
const originalApply = solver.apply.bind(solver);
|
||||
solver.apply = (moveStr) => {
|
||||
// console.log('Applying:', moveStr);
|
||||
originalApply(moveStr);
|
||||
};
|
||||
|
||||
try {
|
||||
const solution = solver.solve();
|
||||
console.log('Solution found:', solution.join(' '));
|
||||
} catch (e) {
|
||||
console.error('Error during solve:', e);
|
||||
}
|
||||
41
test/test_beginner_random.js
Normal file
41
test/test_beginner_random.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
|
||||
import { BeginnerSolver } from "../src/utils/solvers/BeginnerSolver.js";
|
||||
|
||||
const allMoves = Object.keys(MOVES);
|
||||
|
||||
const getRandomScramble = (length = 20) => {
|
||||
let s = [];
|
||||
for (let i = 0; i < length; i++)
|
||||
s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
|
||||
return s.join(" ");
|
||||
};
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
let cube = new DeepCube();
|
||||
const scramble = getRandomScramble();
|
||||
scramble.split(" ").forEach((move) => (cube = cube.multiply(MOVES[move])));
|
||||
|
||||
const startTime = Date.now();
|
||||
const solver = new BeginnerSolver(cube);
|
||||
try {
|
||||
const solution = solver.solve();
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
console.log(
|
||||
`Test ${i}: Solved in ${elapsedTime}ms. Solution length: ${solution.length}`,
|
||||
);
|
||||
|
||||
// Verify it actually solved it
|
||||
let testCube = cube.clone();
|
||||
solution.forEach((m) => (testCube = testCube.multiply(MOVES[m])));
|
||||
if (!solver.isSolvedState(testCube)) {
|
||||
console.error(
|
||||
`ERROR: Test ${i} failed to fully solve the cube mathematically!`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`ERROR: Test ${i} threw an exception:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log("All 20 tests passed flawlessly!");
|
||||
34
test/test_diagnostics.js
Normal file
34
test/test_diagnostics.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
|
||||
|
||||
const allMoves = Object.keys(MOVES);
|
||||
const getRandomScramble = (length = 20) => {
|
||||
let s = [];
|
||||
for (let i = 0; i < length; i++) s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
|
||||
return s.join(' ');
|
||||
};
|
||||
|
||||
let cube = new DeepCube();
|
||||
const scramble = getRandomScramble();
|
||||
scramble.split(' ').forEach(move => cube = cube.multiply(MOVES[move]));
|
||||
|
||||
const solver = new BeginnerSolver(cube);
|
||||
solver.solve();
|
||||
|
||||
console.log("Check Cross:");
|
||||
for (let i of [4, 5, 6, 7]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
|
||||
|
||||
console.log("Check F2L Corners:");
|
||||
for (let i of [4, 5, 6, 7]) console.log(`Corner ${i}: cp=${solver.cube.cp.indexOf(i)} co=${solver.cube.co[solver.cube.cp.indexOf(i)]}`);
|
||||
|
||||
console.log("Check F2L Edges:");
|
||||
for (let i of [8, 9, 10, 11]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
|
||||
|
||||
console.log("Check OLL:");
|
||||
console.log(`co:`, solver.cube.co.slice(0, 4));
|
||||
console.log(`eo:`, solver.cube.eo.slice(0, 4));
|
||||
|
||||
console.log("Check PLL:");
|
||||
console.log(`cp:`, solver.cube.cp.slice(0, 4));
|
||||
console.log(`ep:`, solver.cube.ep.slice(0, 4));
|
||||
|
||||
40
test/test_diagnostics2.js
Normal file
40
test/test_diagnostics2.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
|
||||
|
||||
const allMoves = Object.keys(MOVES);
|
||||
const getRandomScramble = (length = 20) => {
|
||||
let s = [];
|
||||
for (let i = 0; i < length; i++) s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
|
||||
return s.join(' ');
|
||||
};
|
||||
|
||||
for (let iter = 0; iter < 100; iter++) {
|
||||
let cube = new DeepCube();
|
||||
const scramble = getRandomScramble();
|
||||
scramble.split(' ').forEach(move => cube = cube.multiply(MOVES[move]));
|
||||
|
||||
const solver = new BeginnerSolver(cube);
|
||||
solver.solve();
|
||||
|
||||
if (!solver.isSolvedState(solver.cube)) {
|
||||
console.log("FAILED ON SCRAMBLE:", scramble);
|
||||
console.log("Check Cross:");
|
||||
for (let i of [4, 5, 6, 7]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
|
||||
|
||||
console.log("Check F2L Corners:");
|
||||
for (let i of [4, 5, 6, 7]) console.log(`Corner ${i}: cp=${solver.cube.cp.indexOf(i)} co=${solver.cube.co[solver.cube.cp.indexOf(i)]}`);
|
||||
|
||||
console.log("Check F2L Edges:");
|
||||
for (let i of [8, 9, 10, 11]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
|
||||
|
||||
console.log("Check OLL:");
|
||||
console.log(`co:`, solver.cube.co.slice(0, 4));
|
||||
console.log(`eo:`, solver.cube.eo.slice(0, 4));
|
||||
|
||||
console.log("Check PLL:");
|
||||
console.log(`cp:`, solver.cube.cp.slice(0, 4));
|
||||
console.log(`ep:`, solver.cube.ep.slice(0, 4));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log("All 100 tests passed!");
|
||||
24
test/test_macros.js
Normal file
24
test/test_macros.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
|
||||
let cube = new DeepCube();
|
||||
const apply = (str) => {
|
||||
str.split(' ').forEach(m => {
|
||||
cube = cube.multiply(MOVES[m]);
|
||||
});
|
||||
};
|
||||
|
||||
apply("R U R'");
|
||||
console.log("Piece 4 (R U R') is at position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
cube = new DeepCube();
|
||||
apply("R U' R' U R U2 R'");
|
||||
console.log("Piece 4 (Up-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
cube = new DeepCube();
|
||||
apply("R U R'"); // insert front facing
|
||||
console.log("Piece 4 (Front-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
cube = new DeepCube();
|
||||
apply("F' U' F"); // insert left facing
|
||||
console.log("Piece 4 (Side-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
24
test/test_macros2.js
Normal file
24
test/test_macros2.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
|
||||
let cube;
|
||||
const apply = (str) => {
|
||||
str.split(' ').forEach(m => {
|
||||
cube = cube.multiply(MOVES[m]);
|
||||
});
|
||||
};
|
||||
|
||||
const check = (name, alg, initPos, initOri) => {
|
||||
cube = new DeepCube();
|
||||
apply(alg);
|
||||
// We applied alg to a SOLVED cube.
|
||||
// The piece that WAS at 4 (DFR) is now at some position P with orientation O.
|
||||
// To solve it, we would need to reverse the alg.
|
||||
// So if we find a piece at P with orientation O, we apply the reverse alg!
|
||||
console.log(`${name}: Extraction piece 4 is at pos ${cube.cp.indexOf(4)} ori ${cube.co[cube.cp.indexOf(4)]}`);
|
||||
};
|
||||
|
||||
check("R U R'", "R U R'");
|
||||
check("R U' R'", "R U' R'");
|
||||
check("F' U' F", "F' U' F");
|
||||
check("R U2 R' U' R U R'", "R U' R' U R U2 R'");
|
||||
|
||||
10
test/test_macros3.js
Normal file
10
test/test_macros3.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
|
||||
let cube = new DeepCube();
|
||||
const apply = (str) => { str.split(' ').forEach(m => { cube = cube.multiply(MOVES[m]); }); };
|
||||
|
||||
cube = new DeepCube(); apply("F' U F");
|
||||
console.log("F' U F reverse puts piece 4 at pos:", cube.cp.indexOf(4), "ori:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
cube = new DeepCube(); apply("U' F' U F");
|
||||
console.log("U' F' U F reverse puts piece 4 at pos:", cube.cp.indexOf(4), "ori:", cube.co[cube.cp.indexOf(4)]);
|
||||
22
test/test_macros4.js
Normal file
22
test/test_macros4.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
|
||||
let cube = new DeepCube();
|
||||
const apply = (str) => { str.split(' ').forEach(m => { cube = cube.multiply(MOVES[m]); }); };
|
||||
|
||||
const check = (name, alg, expectedPos, expectedOri) => {
|
||||
cube = new DeepCube();
|
||||
apply(alg); // reverse of extraction
|
||||
let p5 = cube.cp.indexOf(5); let o5 = cube.co[p5];
|
||||
console.log(`${name}: pos 5 is ${p5} (expected ${expectedPos}), ori ${o5} (expected ${expectedOri})`);
|
||||
};
|
||||
|
||||
// DLF (5) Target UFL (1)
|
||||
check("F' U' F reverse", "F' U F", 1, 2); // if reverse puts it at pos 1 ori 2, then if at pos 1 ori 2 use F' U' F!
|
||||
check("L U L' reverse", "L U' L'", 1, 1);
|
||||
check("L' U' L reverse", "L' U L", 1, 1); // wait, L' moves DLF to UBL(2)? Let's find out!
|
||||
|
||||
// Check extraction from 5
|
||||
cube = new DeepCube(); apply("L U L'");
|
||||
console.log("Extract DLF (5) with L U L' gives pos:", cube.cp.indexOf(5), "ori:", cube.co[cube.cp.indexOf(5)]);
|
||||
cube = new DeepCube(); apply("F' U' F");
|
||||
console.log("Extract DLF (5) with F' U' F gives pos:", cube.cp.indexOf(5), "ori:", cube.co[cube.cp.indexOf(5)]);
|
||||
22
test/test_moves.js
Normal file
22
test/test_moves.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
|
||||
|
||||
let cube = new DeepCube();
|
||||
const apply = (str) => {
|
||||
str.split(' ').forEach(m => {
|
||||
cube = cube.multiply(MOVES[m]);
|
||||
});
|
||||
};
|
||||
|
||||
// We want to verify `R U R'` extracts piece 4 (DFR) to U layer.
|
||||
apply("R U R'");
|
||||
console.log("Piece 4 is at position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
cube = new DeepCube();
|
||||
// What if piece 4 is at URF (position 0)? We want to insert it to DFR (position 4).
|
||||
// If Yellow is UP, co=0.
|
||||
// Let's create a state where DFR is at URF with co=0.
|
||||
// We can do this by applying R U2 R' U' R U R' IN REVERSE to extract it.
|
||||
// Reverse of R U2 R' U' R U R' is: R U' R' U R U2 R'
|
||||
apply("R U' R' U R U2 R'");
|
||||
console.log("Extraction -> Piece 4 position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
|
||||
|
||||
Reference in New Issue
Block a user