Here is a small Rush Hour board. The red car wants to reach the exit on the right edge. Click a car to select it, then use the arrows to slide it along its lane. A car can only move into empty squares, and only along the direction it is oriented.
<p class="hint">The <b>red</b> car must reach the exit (the gap on the right edge of its row). Click a car to select it, then use the arrows to slide it along its lane. Each move is trivial — but knowing whether the red car can <i>ever</i> get out means searching every reachable board.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Select a car and slide it.</div>
<div class="btns">
<button id="left" type="button">◀</button>
<button id="right" type="button">▶</button>
<button id="up" type="button">▲</button>
<button id="down" type="button">▼</button>
<button id="solve" type="button">Solve</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; }
.board { position: relative; width: 280px; height: 280px; background: #e8eef3;
border: 2px solid #cdd9e3; border-radius: 10px; margin: .4rem 0; }
.exit { position: absolute; right: -2px; width: 6px; height: 46px; background: #0a7d33;
border-radius: 3px; }
.car { position: absolute; border-radius: 8px; cursor: pointer; transition: all .12s ease;
border: 2px solid rgba(0,0,0,.18); }
.car.sel { outline: 3px solid #1d3557; outline-offset: 1px; z-index: 2; }
.car.red { background: #e63946; }
.car.c1 { background: #4895ef; } .car.c2 { background: #f4a261; }
.car.c3 { background: #8d99ae; } .car.c4 { background: #b5838d; }
.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: .4rem; flex-wrap: wrap; }
button { font: 600 15px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; min-width: 42px; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Generalized Rush Hour is PSPACE-complete (Flake & Baum, 2002). On a fixed board
// we just search: every legal slide leads to a new board STATE, and the only sure
// way to know if the red car can escape is a breadth-first search over all reachable states. Watch the explored-states counter climb even on this tiny 5x5 board.
const N = 5, CELL = 56, GAP = 2; // logical grid N x N
// Cars: id, orientation h/v, length, row, col (top-left). ’red’ is horizontal, exits right.
const START = [
{ id:'red', o:'h', len:2, r:2, c:0, cls:'red' },
{ id:'A', o:'v', len:2, r:1, c:2, cls:'c1' },
{ id:'B', o:'v', len:2, r:0, c:3, cls:'c2' },
{ id:'C', o:'h', len:2, r:4, c:0, cls:'c3' },
{ id:'D', o:'v', len:2, r:2, c:4, cls:'c4' },
];
let cars = clone(START), sel = 'red';
function clone(cs){ return cs.map(c => ({...c})); }
// Occupancy grid for a configuration (excluding optionally one car).
function grid(cs, skip){
const g = Array.from({length:N}, () => Array(N).fill(null));
for(const c of cs){ if(c.id===skip) continue;
for(let i=0;i<c.len;i++){ const r=c.r+(c.o==='v'?i:0), col=c.c+(c.o==='h'?i:0); g[r][col]=c.id; } }
return g;
}
// Can car `id` move by step (-1/+1) along its lane? Returns the moved car list or null.
function move(cs, id, step){
const c = cs.find(x=>x.id===id); if(!c) return null;
const g = grid(cs, id);
let nr=c.r, nc=c.c;
if(c.o==='h') nc+=step; else nr+=step;
// the single new leading cell that gets occupied
const lead = step>0
? (c.o==='h' ? [c.r, c.c+c.len] : [c.r+c.len, c.c])
: (c.o==='h' ? [c.r, c.c-1] : [c.r-1, c.c]);
const [lr,lc] = lead;
if(lr<0||lr>=N||lc<0||lc>=N) return null;
if(g[lr][lc]!==null) return null;
const out = clone(cs); const m = out.find(x=>x.id===id); m.r=nr; m.c=nc; return out;
}
// Solved iff red’s right edge touches the exit column (col N-1 occupied by red’s tail).
function solved(cs){ const red=cs.find(x=>x.id==='red'); return red.c+red.len-1 === N-1; }
function key(cs){ return cs.map(c=>c.id+c.r+','+c.c).sort().join('|'); }
// BFS over board states; returns {steps, path} where steps = states explored.
function bfs(cs){
const start=key(cs); const seen=new Set([start]);
const q=[{cs:clone(cs), path:[]}]; let explored=0;
while(q.length){
const cur=q.shift(); explored++;
if(solved(cur.cs)) return {explored, path:cur.path, win:true};
for(const car of cur.cs){
for(const step of [-1,1]){
const nxt=move(cur.cs, car.id, step);
if(!nxt) continue;
const k=key(nxt);
if(seen.has(k)) continue;
seen.add(k);
q.push({cs:nxt, path:[...cur.path, [car.id, step]]});
}
}
}
return {explored, path:null, win:false};
}
const boardEl=document.getElementById('board');
function render(){
boardEl.innerHTML='';
const exit=document.createElement('div'); exit.className='exit';
exit.style.top=(2*CELL+GAP)+'px'; boardEl.appendChild(exit);
for(const c of cars){
const el=document.createElement('div');
el.className='car '+c.cls+(c.id===sel?' sel':'');
const w=(c.o==='h'?c.len:1), h=(c.o==='v'?c.len:1);
el.style.left=(c.c*CELL+GAP)+'px'; el.style.top=(c.r*CELL+GAP)+'px';
el.style.width=(w*CELL-2*GAP)+'px'; el.style.height=(h*CELL-2*GAP)+'px';
el.addEventListener('click',()=>{ sel=c.id; render(); });
boardEl.appendChild(el);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function tryMove(step, vertical){
const c=cars.find(x=>x.id===sel); if(!c) return;
if((vertical && c.o!=='v') || (!vertical && c.o!=='h')){ setStatus('That car can’t move that way.','bad'); return; }
const nxt=move(cars, sel, step);
if(!nxt){ setStatus('Blocked — something is in the way.','bad'); return; }
cars=nxt;
if(solved(cars)){ render(); setStatus('✓ The red car escaped!','ok'); return; }
render(); setStatus('Keep sliding to free the red car.','');
}
document.getElementById('left').addEventListener('click',()=>tryMove(-1,false));
document.getElementById('right').addEventListener('click',()=>tryMove(1,false));
document.getElementById('up').addEventListener('click',()=>tryMove(-1,true));
document.getElementById('down').addEventListener('click',()=>tryMove(1,true));
document.getElementById('reset').addEventListener('click',()=>{ cars=clone(START); sel='red'; render(); setStatus('Select a car and slide it.',''); });
document.getElementById('solve').addEventListener('click',()=>{
const res=bfs(cars);
if(!res.win){ setStatus(fmt('No escape exists — explored %explored% states.', { explored: res.explored }),'bad'); return; }
// replay the solution path
let i=0;
setStatus(fmt('Solved in %length% moves — explored %explored% board states.', { length: res.path.length, explored: res.explored }),'ok');
console.log('Solution:', res.path.map(p=>p[0]+(p[1]>0?'+':'-')).join(' '), '| explored', res.explored, 'states');
const timer=setInterval(()=>{
if(i>=res.path.length){ clearInterval(timer); return; }
const [id,step]=res.path[i++]; const nxt=move(cars,id,step); if(nxt){ cars=nxt; sel=id; render(); }
}, 350);
});
render(); setStatus('Select a car and slide it.','');
<p class="hint">The <b>red</b> car must reach the exit (the gap on the right edge of its row). Click a car to select it, then use the arrows to slide it along its lane. Each move is trivial — but knowing whether the red car can <i>ever</i> get out means searching every reachable board.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Select a car and slide it.</div>
<div class="btns">
<button id="left" type="button">◀</button>
<button id="right" type="button">▶</button>
<button id="up" type="button">▲</button>
<button id="down" type="button">▼</button>
<button id="solve" type="button">Solve</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; }
.board { position: relative; width: 280px; height: 280px; background: #e8eef3;
border: 2px solid #cdd9e3; border-radius: 10px; margin: .4rem 0; }
.exit { position: absolute; right: -2px; width: 6px; height: 46px; background: #0a7d33;
border-radius: 3px; }
.car { position: absolute; border-radius: 8px; cursor: pointer; transition: all .12s ease;
border: 2px solid rgba(0,0,0,.18); }
.car.sel { outline: 3px solid #1d3557; outline-offset: 1px; z-index: 2; }
.car.red { background: #e63946; }
.car.c1 { background: #4895ef; } .car.c2 { background: #f4a261; }
.car.c3 { background: #8d99ae; } .car.c4 { background: #b5838d; }
.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: .4rem; flex-wrap: wrap; }
button { font: 600 15px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; min-width: 42px; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Generalized Rush Hour is PSPACE-complete (Flake & Baum, 2002). On a fixed board
// we just search: every legal slide leads to a new board STATE, and the only sure
// way to know if the red car can escape is a breadth-first search over all reachable states. Watch the explored-states counter climb even on this tiny 5x5 board.
const N = 5, CELL = 56, GAP = 2; // logical grid N x N
// Cars: id, orientation h/v, length, row, col (top-left). ’red’ is horizontal, exits right.
const START = [
{ id:'red', o:'h', len:2, r:2, c:0, cls:'red' },
{ id:'A', o:'v', len:2, r:1, c:2, cls:'c1' },
{ id:'B', o:'v', len:2, r:0, c:3, cls:'c2' },
{ id:'C', o:'h', len:2, r:4, c:0, cls:'c3' },
{ id:'D', o:'v', len:2, r:2, c:4, cls:'c4' },
];
let cars = clone(START), sel = 'red';
function clone(cs){ return cs.map(c => ({...c})); }
// Occupancy grid for a configuration (excluding optionally one car).
function grid(cs, skip){
const g = Array.from({length:N}, () => Array(N).fill(null));
for(const c of cs){ if(c.id===skip) continue;
for(let i=0;i<c.len;i++){ const r=c.r+(c.o==='v'?i:0), col=c.c+(c.o==='h'?i:0); g[r][col]=c.id; } }
return g;
}
// Can car `id` move by step (-1/+1) along its lane? Returns the moved car list or null.
function move(cs, id, step){
const c = cs.find(x=>x.id===id); if(!c) return null;
const g = grid(cs, id);
let nr=c.r, nc=c.c;
if(c.o==='h') nc+=step; else nr+=step;
// the single new leading cell that gets occupied
const lead = step>0
? (c.o==='h' ? [c.r, c.c+c.len] : [c.r+c.len, c.c])
: (c.o==='h' ? [c.r, c.c-1] : [c.r-1, c.c]);
const [lr,lc] = lead;
if(lr<0||lr>=N||lc<0||lc>=N) return null;
if(g[lr][lc]!==null) return null;
const out = clone(cs); const m = out.find(x=>x.id===id); m.r=nr; m.c=nc; return out;
}
// Solved iff red’s right edge touches the exit column (col N-1 occupied by red’s tail).
function solved(cs){ const red=cs.find(x=>x.id==='red'); return red.c+red.len-1 === N-1; }
function key(cs){ return cs.map(c=>c.id+c.r+','+c.c).sort().join('|'); }
// BFS over board states; returns {steps, path} where steps = states explored.
function bfs(cs){
const start=key(cs); const seen=new Set([start]);
const q=[{cs:clone(cs), path:[]}]; let explored=0;
while(q.length){
const cur=q.shift(); explored++;
if(solved(cur.cs)) return {explored, path:cur.path, win:true};
for(const car of cur.cs){
for(const step of [-1,1]){
const nxt=move(cur.cs, car.id, step);
if(!nxt) continue;
const k=key(nxt);
if(seen.has(k)) continue;
seen.add(k);
q.push({cs:nxt, path:[...cur.path, [car.id, step]]});
}
}
}
return {explored, path:null, win:false};
}
const boardEl=document.getElementById('board');
function render(){
boardEl.innerHTML='';
const exit=document.createElement('div'); exit.className='exit';
exit.style.top=(2*CELL+GAP)+'px'; boardEl.appendChild(exit);
for(const c of cars){
const el=document.createElement('div');
el.className='car '+c.cls+(c.id===sel?' sel':'');
const w=(c.o==='h'?c.len:1), h=(c.o==='v'?c.len:1);
el.style.left=(c.c*CELL+GAP)+'px'; el.style.top=(c.r*CELL+GAP)+'px';
el.style.width=(w*CELL-2*GAP)+'px'; el.style.height=(h*CELL-2*GAP)+'px';
el.addEventListener('click',()=>{ sel=c.id; render(); });
boardEl.appendChild(el);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function tryMove(step, vertical){
const c=cars.find(x=>x.id===sel); if(!c) return;
if((vertical && c.o!=='v') || (!vertical && c.o!=='h')){ setStatus('That car can’t move that way.','bad'); return; }
const nxt=move(cars, sel, step);
if(!nxt){ setStatus('Blocked — something is in the way.','bad'); return; }
cars=nxt;
if(solved(cars)){ render(); setStatus('✓ The red car escaped!','ok'); return; }
render(); setStatus('Keep sliding to free the red car.','');
}
document.getElementById('left').addEventListener('click',()=>tryMove(-1,false));
document.getElementById('right').addEventListener('click',()=>tryMove(1,false));
document.getElementById('up').addEventListener('click',()=>tryMove(-1,true));
document.getElementById('down').addEventListener('click',()=>tryMove(1,true));
document.getElementById('reset').addEventListener('click',()=>{ cars=clone(START); sel='red'; render(); setStatus('Select a car and slide it.',''); });
document.getElementById('solve').addEventListener('click',()=>{
const res=bfs(cars);
if(!res.win){ setStatus(fmt('No escape exists — explored %explored% states.', { explored: res.explored }),'bad'); return; }
// replay the solution path
let i=0;
setStatus(fmt('Solved in %length% moves — explored %explored% board states.', { length: res.path.length, explored: res.explored }),'ok');
console.log('Solution:', res.path.map(p=>p[0]+(p[1]>0?'+':'-')).join(' '), '| explored', res.explored, 'states');
const timer=setInterval(()=>{
if(i>=res.path.length){ clearInterval(timer); return; }
const [id,step]=res.path[i++]; const nxt=move(cars,id,step); if(nxt){ cars=nxt; sel=id; render(); }
}, 350);
});
render(); setStatus('Select a car and slide it.','');
Comments
Loading comments...