// Logical Rubik's cube state, decoupled from Three.js. // Faces: U(up), D(down), L(left), R(right), F(front), B(back). // Each face is a flat array of 9 sticker colors (same letter as its solved face). // Sticker indices, from the viewer's perspective looking at that face: // 0 1 2 // 3 4 5 // 6 7 8 export type FaceKey = 'U' | 'D' | 'L' | 'R' | 'F' | 'B'; export type Color = FaceKey; export type State = Record; export function solvedState(): State { const mk = (c: Color): Color[] => Array(9).fill(c); return { U: mk('U'), D: mk('D'), L: mk('L'), R: mk('R'), F: mk('F'), B: mk('B') }; } function rotateFaceCW(face: Color[]): Color[] { const f = face; return [f[6], f[3], f[0], f[7], f[4], f[1], f[8], f[5], f[2]]; } function rotateFaceCCW(face: Color[]): Color[] { const f = face; return [f[2], f[5], f[8], f[1], f[4], f[7], f[0], f[3], f[6]]; } // A "cycle" describes which (face, indices) move where for a CW quarter turn of a face layer. // Each entry: 4 ordered groups of stickers; each turn shifts group i -> group i+1 (mod 4). type Cycle = Array<[FaceKey, number[]]>; const CYCLES: Record = { U: [ ['F', [0, 1, 2]], ['L', [0, 1, 2]], ['B', [0, 1, 2]], ['R', [0, 1, 2]] ], D: [ ['F', [6, 7, 8]], ['R', [6, 7, 8]], ['B', [6, 7, 8]], ['L', [6, 7, 8]] ], R: [ ['U', [2, 5, 8]], ['B', [6, 3, 0]], ['D', [2, 5, 8]], ['F', [2, 5, 8]] ], L: [ ['U', [0, 3, 6]], ['F', [0, 3, 6]], ['D', [0, 3, 6]], ['B', [8, 5, 2]] ], F: [ ['U', [6, 7, 8]], ['R', [0, 3, 6]], ['D', [2, 1, 0]], ['L', [8, 5, 2]] ], B: [ ['U', [2, 1, 0]], ['L', [0, 3, 6]], ['D', [6, 7, 8]], ['R', [8, 5, 2]] ] }; function applyFaceTurn(state: State, face: FaceKey, prime: boolean): void { // The turned face's own stickers rotate opposite to the array-index sense: // a non-prime turn maps new[6] = old[0] in viewer indexing, which is rotateFaceCCW. state[face] = prime ? rotateFaceCW(state[face]) : rotateFaceCCW(state[face]); const cycle = CYCLES[face]; const order = prime ? [3, 2, 1, 0] : [0, 1, 2, 3]; const groups = order.map(i => cycle[i]); const tmp = groups[0][1].map(idx => state[groups[0][0]][idx]); for (let i = 0; i < 3; i++) { const [fromFace, fromIdx] = groups[i + 1]; const [toFace, toIdx] = groups[i]; for (let k = 0; k < 3; k++) state[toFace][toIdx[k]] = state[fromFace][fromIdx[k]]; } const [lastFace, lastIdx] = groups[3]; for (let k = 0; k < 3; k++) state[lastFace][lastIdx[k]] = tmp[k]; } // Slice moves expressed via face turns + whole-cube rotation. All three layers of // an axis commute (same rotation axis), so e.g. M = x' with the R and L layers // rotated back: M = R L' x'. Each pair below is exact inverses. const COMPOUND: Record = { M: ["R", "L'", "x'"], // middle layer (x=0) follows L "M'": ["R'", "L", "x"], E: ["U", "D'", "y'"], // equatorial layer (y=0) follows D "E'": ["U'", "D", "y"], S: ["F'", "B", "z"], // standing layer (z=0) follows F "S'": ["F", "B'", "z'"] }; // Whole-cube rotation: permute face arrays + rotate each face. // Direction convention matches the visual animator (CW looking from the +axis // toward the origin), so the logical state stays in sync with the rendered cube. // x: CW from +X. The up sticker moves to front: U->F->D->B->U. function rotX(state: State, prime: boolean): void { const s = state; if (!prime) { const F = s.F, U = s.U, B = s.B, D = s.D; s.F = U; s.U = rotateFaceCW(rotateFaceCW(B)); s.B = rotateFaceCW(rotateFaceCW(D)); s.D = F; s.R = rotateFaceCCW(s.R); s.L = rotateFaceCW(s.L); } else { const F = s.F, U = s.U, B = s.B, D = s.D; s.U = F; s.B = rotateFaceCW(rotateFaceCW(U)); s.D = rotateFaceCW(rotateFaceCW(B)); s.F = D; s.R = rotateFaceCW(s.R); s.L = rotateFaceCCW(s.L); } } // y: CW from +Y. The front sticker moves to right: F->R->B->L->F. function rotY(state: State, prime: boolean): void { const s = state; if (!prime) { const F = s.F, L = s.L, B = s.B, R = s.R; s.F = L; s.L = B; s.B = R; s.R = F; s.U = rotateFaceCCW(s.U); s.D = rotateFaceCW(s.D); } else { const F = s.F, L = s.L, B = s.B, R = s.R; s.L = F; s.B = L; s.R = B; s.F = R; s.U = rotateFaceCW(s.U); s.D = rotateFaceCCW(s.D); } } // z: CW from +Z. The up sticker moves to left: U->L->D->R->U. function rotZ(state: State, prime: boolean): void { const s = state; if (!prime) { const U = s.U, R = s.R, D = s.D, L = s.L; s.L = rotateFaceCCW(U); s.U = rotateFaceCCW(R); s.R = rotateFaceCCW(D); s.D = rotateFaceCCW(L); s.F = rotateFaceCCW(s.F); s.B = rotateFaceCW(s.B); } else { const U = s.U, R = s.R, D = s.D, L = s.L; s.R = rotateFaceCW(U); s.D = rotateFaceCW(R); s.L = rotateFaceCW(D); s.U = rotateFaceCW(L); s.F = rotateFaceCW(s.F); s.B = rotateFaceCCW(s.B); } } export function applyMove(state: State, move: string): void { if (!move) return; if (COMPOUND[move]) { for (const m of COMPOUND[move]) applyMove(state, m); return; } const prime = move.endsWith("'"); const base = prime ? move.slice(0, -1) : move; if (base === 'x') return rotX(state, prime); if (base === 'y') return rotY(state, prime); if (base === 'z') return rotZ(state, prime); if ('UDLRFB'.includes(base)) return applyFaceTurn(state, base as FaceKey, prime); } export function isSolved(state: State): boolean { return (Object.keys(state) as FaceKey[]).every(f => state[f].every(c => c === state[f][4]) ); } export function cloneState(s: State): State { return { U: [...s.U], D: [...s.D], L: [...s.L], R: [...s.R], F: [...s.F], B: [...s.B] }; }