<p class="hint">A mini "cube": eight colored stickers on a ring. Three moves shuffle them — <b>L</b> rotates the ring left, <b>R</b> rotates it right, <b>S</b> swaps the top two. Scramble it, then bring all colors back to order. Checking a solution is a glance; finding the <i>shortest</i> one is a search.</p>
<div id="ring" class="ring"></div>
<div class="status" id="status">Press Scramble to begin.</div>
<div class="btns">
<button id="mL" type="button">L ↺</button>
<button id="mR" type="button">R ↻</button>
<button id="mS" type="button">S ⇅</button>
<button id="scramble" type="button" class="alt">Scramble</button>
<button id="find" type="button">Find shortest solution</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.ring { display: grid; grid-template-columns: repeat(8, 40px); gap: 5px; margin: .6rem 0; }
.st { width: 40px; height: 40px; border-radius: 8px; border: 1px solid rgba(0,0,0,.18);
display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; color: rgba(0,0,0,.45); }
.c0 { background: #e63946; } .c1 { background: #f4a261; } .c2 { background: #e9c46a; }
.c3 { background: #2a9d8f; } .c4 { background: #277da1; } .c5 { background: #5e548e; }
.c6 { background: #b5179e; } .c7 { background: #adb5bd; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.alt { background: #2a9d8f; border-color: #21847a; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A toy "cube": 8 stickers on a ring. Three generators shuffle the state:
// L = rotate the ring one step left, R = rotate one step right, S = swap pos 0 & 1.
// The SOLVED state is [0,1,2,3,4,5,6,7]. Like a real cube, CHECKING a sequence is a quick replay, but FINDING the SHORTEST solution means searching the reachable states.
// We use breadth-first search (BFS) = the optimal-move oracle, the same idea
// that, scaled up, makes the generalized cube NP-complete (Demaine et al., 2017).
const N = 8;
const SOLVED = [0,1,2,3,4,5,6,7];
let state = SOLVED.slice();
const L = s => s.slice(1).concat(s[0]); // rotate left
const R = s => [s[N-1]].concat(s.slice(0, N-1)); // rotate right
const S = s => { const t = s.slice(); [t[0], t[1]] = [t[1], t[0]]; return t; };
const MOVES = { L, R, S };
function render() {
const r = document.getElementById('ring'); r.innerHTML = '';
state.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'st c' + v; d.textContent = i; // index label shows target position
r.appendChild(d);
});
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
const solved = s => s.every((v, i) => v === i);
function apply(name) {
state = MOVES[name](state); render();
setStatus(solved(state) ? '✓ Solved! Every sticker is home.' : 'Keep going…', solved(state) ? 'ok' : '');
}
function scramble() {
const names = ['L','R','S']; let n = 12;
while (n--) state = MOVES[names[Math.floor(Math.random()*3)]](state);
if (solved(state)) state = L(state); // never hand back an already-solved scramble
render(); setStatus('Scrambled. Bring the colors back to order.', '');
}
// BFS from the current state to SOLVED: shortest solution by construction.
function find() {
const start = state.slice();
if (solved(start)) { setStatus('Already solved — 0 moves needed.', 'ok'); return; }
const seen = new Set([start.join(',')]);
let frontier = [{ s: start, path: [] }], depth = 0, visited = 0;
while (frontier.length) {
const next = [];
for (const node of frontier) {
visited++;
for (const name of ['L','R','S']) {
const ns = MOVES[name](node.s), key = ns.join(',');
if (seen.has(key)) continue;
const path = node.path.concat(name);
if (solved(ns)) {
setStatus(fmt('Shortest solution: %path% (%length% moves, %visited% states explored)', { path: path.join(' '), length: path.length, visited: visited }), 'ok');
console.log('shortest', path.join(''), 'len', path.length, 'visited', visited);
return;
}
seen.add(key); next.push({ s: ns, path });
}
}
frontier = next; depth++;
if (depth > 40) break;
}
setStatus('No solution found (unreachable).', 'bad');
}
document.getElementById('mL').addEventListener('click', () => apply('L'));
document.getElementById('mR').addEventListener('click', () => apply('R'));
document.getElementById('mS').addEventListener('click', () => apply('S'));
document.getElementById('scramble').addEventListener('click', scramble);
document.getElementById('find').addEventListener('click', find);
document.getElementById('reset').addEventListener('click', () => { state = SOLVED.slice(); render(); setStatus('Reset to solved.', 'ok'); });
render();
<p class="hint">A mini "cube": eight colored stickers on a ring. Three moves shuffle them — <b>L</b> rotates the ring left, <b>R</b> rotates it right, <b>S</b> swaps the top two. Scramble it, then bring all colors back to order. Checking a solution is a glance; finding the <i>shortest</i> one is a search.</p>
<div id="ring" class="ring"></div>
<div class="status" id="status">Press Scramble to begin.</div>
<div class="btns">
<button id="mL" type="button">L ↺</button>
<button id="mR" type="button">R ↻</button>
<button id="mS" type="button">S ⇅</button>
<button id="scramble" type="button" class="alt">Scramble</button>
<button id="find" type="button">Find shortest solution</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.ring { display: grid; grid-template-columns: repeat(8, 40px); gap: 5px; margin: .6rem 0; }
.st { width: 40px; height: 40px; border-radius: 8px; border: 1px solid rgba(0,0,0,.18);
display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; color: rgba(0,0,0,.45); }
.c0 { background: #e63946; } .c1 { background: #f4a261; } .c2 { background: #e9c46a; }
.c3 { background: #2a9d8f; } .c4 { background: #277da1; } .c5 { background: #5e548e; }
.c6 { background: #b5179e; } .c7 { background: #adb5bd; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.alt { background: #2a9d8f; border-color: #21847a; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A toy "cube": 8 stickers on a ring. Three generators shuffle the state:
// L = rotate the ring one step left, R = rotate one step right, S = swap pos 0 & 1.
// The SOLVED state is [0,1,2,3,4,5,6,7]. Like a real cube, CHECKING a sequence is a quick replay, but FINDING the SHORTEST solution means searching the reachable states.
// We use breadth-first search (BFS) = the optimal-move oracle, the same idea
// that, scaled up, makes the generalized cube NP-complete (Demaine et al., 2017).
const N = 8;
const SOLVED = [0,1,2,3,4,5,6,7];
let state = SOLVED.slice();
const L = s => s.slice(1).concat(s[0]); // rotate left
const R = s => [s[N-1]].concat(s.slice(0, N-1)); // rotate right
const S = s => { const t = s.slice(); [t[0], t[1]] = [t[1], t[0]]; return t; };
const MOVES = { L, R, S };
function render() {
const r = document.getElementById('ring'); r.innerHTML = '';
state.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'st c' + v; d.textContent = i; // index label shows target position
r.appendChild(d);
});
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
const solved = s => s.every((v, i) => v === i);
function apply(name) {
state = MOVES[name](state); render();
setStatus(solved(state) ? '✓ Solved! Every sticker is home.' : 'Keep going…', solved(state) ? 'ok' : '');
}
function scramble() {
const names = ['L','R','S']; let n = 12;
while (n--) state = MOVES[names[Math.floor(Math.random()*3)]](state);
if (solved(state)) state = L(state); // never hand back an already-solved scramble
render(); setStatus('Scrambled. Bring the colors back to order.', '');
}
// BFS from the current state to SOLVED: shortest solution by construction.
function find() {
const start = state.slice();
if (solved(start)) { setStatus('Already solved — 0 moves needed.', 'ok'); return; }
const seen = new Set([start.join(',')]);
let frontier = [{ s: start, path: [] }], depth = 0, visited = 0;
while (frontier.length) {
const next = [];
for (const node of frontier) {
visited++;
for (const name of ['L','R','S']) {
const ns = MOVES[name](node.s), key = ns.join(',');
if (seen.has(key)) continue;
const path = node.path.concat(name);
if (solved(ns)) {
setStatus(fmt('Shortest solution: %path% (%length% moves, %visited% states explored)', { path: path.join(' '), length: path.length, visited: visited }), 'ok');
console.log('shortest', path.join(''), 'len', path.length, 'visited', visited);
return;
}
seen.add(key); next.push({ s: ns, path });
}
}
frontier = next; depth++;
if (depth > 40) break;
}
setStatus('No solution found (unreachable).', 'bad');
}
document.getElementById('mL').addEventListener('click', () => apply('L'));
document.getElementById('mR').addEventListener('click', () => apply('R'));
document.getElementById('mS').addEventListener('click', () => apply('S'));
document.getElementById('scramble').addEventListener('click', scramble);
document.getElementById('find').addEventListener('click', find);
document.getElementById('reset').addEventListener('click', () => { state = SOLVED.slice(); render(); setStatus('Reset to solved.', 'ok'); });
render();
Notice the asymmetry. Checking a solution is instant: replay the moves and see if every sticker is home. Finding the shortest solution is the hard part — press Find shortest solution and the computer searches outward from the scramble, trying every move sequence until it stumbles onto the goal. Each extra move multiplies the number of sequences it must explore. On a real 3×3 that explosion is the difference between "always ≤20 moves exist" and "good luck finding them by brute force."
Comments
Loading comments...