Here is a tiny level. Mario starts on the left; the flag is on the right. Between them are doors that are toggled by switches Mario steps on. Move with the buttons and try to find a path to the flag — then let the computer search for you.
<p class="hint">Mario starts on the left; reach the <b>flag</b> on the right. Stepping on a colored <b>switch</b> toggles the matching <b>door</b> (a closed door blocks you). A passage you opened can slam shut again — so the order of your moves matters. Checking a walkthrough is instant; <b>searching</b> for one explodes as switches multiply.</p>
<div id="level" class="level"></div>
<div class="status" id="status">Use ◀ ▶ to walk. Step on switches to toggle doors.</div>
<div class="btns">
<button id="left" type="button">◀ Move left</button>
<button id="right" type="button">▶ Move right</button>
<button id="solve" type="button">Let the computer search</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; }
.level { display: grid; grid-template-columns: repeat(9, 44px); gap: 4px; margin: .5rem 0; }
.tile { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 20px ui-monospace, monospace; border-radius: 8px; user-select: none;
border: 1px solid #cdd9e3; background: #eef4f8; }
.tile.path { background: #dfeaf2; }
.tile.switch { background: #fff4d6; border-color: #e8c766; }
.tile.flag { background: #d8f3df; border-color: #8fd6a6; }
.tile.door { background: #c9ccd1; border-color: #adb1b8; }
.tile.door.open { background: #eef4f8; border-color: #cdd9e3; }
.tile.mario { box-shadow: inset 0 0 0 3px #e63946; }
.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 .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Generalized Super Mario reachability (1-D slice). The level is a row of tiles.
// A "configuration" = Mario's position AND the open/closed state of every door.
// Stepping on a switch flips its door. A closed door cannot be entered.
// Question: from the start config, is the flag reachable? This is the PSPACE-shaped
// problem (Demaine, Viglietta, Williams). The computer answers it with a BFS over the
// graph of configurations — whose size is (tiles) x 2^(#doors).
const N = 9;
const START = 0, FLAG = 8;
// tile -> {switch: doorId} steps that flip a door when entered.
const SWITCHES = { 2: 0, 5: 1 }; // tile 2 toggles door A, tile 5 toggles door B
const DOORS = { 4: 0, 7: 1 }; // tile 4 = door A, tile 7 = door B
// Initial door states (false = closed). Door A starts CLOSED, door B starts OPEN.
const INIT = [false, true];
const NDOORS = INIT.length;
let pos = START;
let doors = INIT.slice();
let lastSolution = null;
const doorOf = (t) => (t in DOORS) ? DOORS[t] : -1;
const isOpen = (t, d) => { const id = doorOf(t); return id < 0 ? true : d[id]; };
// Apply entering a tile: if it carries a switch, toggle that door.
function enter(t, d){ const nd = d.slice(); if (t in SWITCHES) nd[SWITCHES[t]] = !nd[SWITCHES[t]]; return nd; }
function step(dir){
const t = pos + dir;
if (t < 0 || t >= N) return;
if (!isOpen(t, doors)) { setStatus('A closed door blocks you there.', 'bad'); return; }
doors = enter(t, doors); pos = t;
if (pos === FLAG) setStatus('You reached the flag!', 'ok');
else setStatus('', '');
render();
}
function render(){
const lv = document.getElementById('level'); lv.innerHTML = '';
for (let t = 0; t < N; t++){
const d = document.createElement('div'); d.className = 'tile';
if (t in SWITCHES) d.classList.add('switch');
if (t in DOORS){ d.classList.add('door'); if (isOpen(t, doors)) d.classList.add('open'); }
if (t === FLAG) d.classList.add('flag');
if (t === pos) d.classList.add('mario');
if (t in SWITCHES) d.textContent = 'S';
else if (t in DOORS) d.textContent = isOpen(t, doors) ? '·' : '▮';
else if (t === FLAG) d.textContent = '⚑';
else if (t === pos) d.textContent = 'M';
lv.appendChild(d);
}
if (document.getElementById('level').querySelector('.mario') === null){}
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t || 'Use ◀ ▶ to walk. Step on switches to toggle doors.'; s.className = 'status ' + cls; }
// BFS over configurations: state = position + door bitmask.
function encode(p, d){ let m = 0; for (let i = 0; i < NDOORS; i++) if (d[i]) m |= (1 << i); return p + ':' + m; }
function solve(){
const startKey = encode(START, INIT);
const seen = new Set([startKey]);
const prev = new Map();
const queue = [{ p: START, d: INIT.slice() }];
let explored = 0, goal = null;
while (queue.length){
const cur = queue.shift(); explored++;
if (cur.p === FLAG){ goal = cur; break; }
for (const dir of [-1, 1]){
const t = cur.p + dir;
if (t < 0 || t >= N) continue;
if (!isOpen(t, cur.d)) continue;
const nd = enter(t, cur.d);
const key = encode(t, nd);
if (!seen.has(key)){ seen.add(key); prev.set(key, encode(cur.p, cur.d)); queue.push({ p: t, d: nd }); }
}
}
const total = N * (1 << NDOORS);
if (goal){
lastSolution = goal;
pos = goal.p; doors = goal.d; render();
setStatus(fmt('Reachable! BFS explored %explored% of %total% configurations.', { explored: explored, total: total }), 'ok');
console.log('Flag reachable. Explored', explored, 'of', total, 'configurations.');
} else {
setStatus('The flag is NOT reachable from the start — no sequence of moves works.', 'bad');
console.log('Flag unreachable. Explored', explored, 'of', total, 'configurations.');
}
}
document.getElementById('left').addEventListener('click', () => step(-1));
document.getElementById('right').addEventListener('click', () => step(1));
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => { pos = START; doors = INIT.slice(); render(); setStatus('', ''); });
render();
<p class="hint">Mario starts on the left; reach the <b>flag</b> on the right. Stepping on a colored <b>switch</b> toggles the matching <b>door</b> (a closed door blocks you). A passage you opened can slam shut again — so the order of your moves matters. Checking a walkthrough is instant; <b>searching</b> for one explodes as switches multiply.</p>
<div id="level" class="level"></div>
<div class="status" id="status">Use ◀ ▶ to walk. Step on switches to toggle doors.</div>
<div class="btns">
<button id="left" type="button">◀ Move left</button>
<button id="right" type="button">▶ Move right</button>
<button id="solve" type="button">Let the computer search</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; }
.level { display: grid; grid-template-columns: repeat(9, 44px); gap: 4px; margin: .5rem 0; }
.tile { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 20px ui-monospace, monospace; border-radius: 8px; user-select: none;
border: 1px solid #cdd9e3; background: #eef4f8; }
.tile.path { background: #dfeaf2; }
.tile.switch { background: #fff4d6; border-color: #e8c766; }
.tile.flag { background: #d8f3df; border-color: #8fd6a6; }
.tile.door { background: #c9ccd1; border-color: #adb1b8; }
.tile.door.open { background: #eef4f8; border-color: #cdd9e3; }
.tile.mario { box-shadow: inset 0 0 0 3px #e63946; }
.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 .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Generalized Super Mario reachability (1-D slice). The level is a row of tiles.
// A "configuration" = Mario's position AND the open/closed state of every door.
// Stepping on a switch flips its door. A closed door cannot be entered.
// Question: from the start config, is the flag reachable? This is the PSPACE-shaped
// problem (Demaine, Viglietta, Williams). The computer answers it with a BFS over the
// graph of configurations — whose size is (tiles) x 2^(#doors).
const N = 9;
const START = 0, FLAG = 8;
// tile -> {switch: doorId} steps that flip a door when entered.
const SWITCHES = { 2: 0, 5: 1 }; // tile 2 toggles door A, tile 5 toggles door B
const DOORS = { 4: 0, 7: 1 }; // tile 4 = door A, tile 7 = door B
// Initial door states (false = closed). Door A starts CLOSED, door B starts OPEN.
const INIT = [false, true];
const NDOORS = INIT.length;
let pos = START;
let doors = INIT.slice();
let lastSolution = null;
const doorOf = (t) => (t in DOORS) ? DOORS[t] : -1;
const isOpen = (t, d) => { const id = doorOf(t); return id < 0 ? true : d[id]; };
// Apply entering a tile: if it carries a switch, toggle that door.
function enter(t, d){ const nd = d.slice(); if (t in SWITCHES) nd[SWITCHES[t]] = !nd[SWITCHES[t]]; return nd; }
function step(dir){
const t = pos + dir;
if (t < 0 || t >= N) return;
if (!isOpen(t, doors)) { setStatus('A closed door blocks you there.', 'bad'); return; }
doors = enter(t, doors); pos = t;
if (pos === FLAG) setStatus('You reached the flag!', 'ok');
else setStatus('', '');
render();
}
function render(){
const lv = document.getElementById('level'); lv.innerHTML = '';
for (let t = 0; t < N; t++){
const d = document.createElement('div'); d.className = 'tile';
if (t in SWITCHES) d.classList.add('switch');
if (t in DOORS){ d.classList.add('door'); if (isOpen(t, doors)) d.classList.add('open'); }
if (t === FLAG) d.classList.add('flag');
if (t === pos) d.classList.add('mario');
if (t in SWITCHES) d.textContent = 'S';
else if (t in DOORS) d.textContent = isOpen(t, doors) ? '·' : '▮';
else if (t === FLAG) d.textContent = '⚑';
else if (t === pos) d.textContent = 'M';
lv.appendChild(d);
}
if (document.getElementById('level').querySelector('.mario') === null){}
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t || 'Use ◀ ▶ to walk. Step on switches to toggle doors.'; s.className = 'status ' + cls; }
// BFS over configurations: state = position + door bitmask.
function encode(p, d){ let m = 0; for (let i = 0; i < NDOORS; i++) if (d[i]) m |= (1 << i); return p + ':' + m; }
function solve(){
const startKey = encode(START, INIT);
const seen = new Set([startKey]);
const prev = new Map();
const queue = [{ p: START, d: INIT.slice() }];
let explored = 0, goal = null;
while (queue.length){
const cur = queue.shift(); explored++;
if (cur.p === FLAG){ goal = cur; break; }
for (const dir of [-1, 1]){
const t = cur.p + dir;
if (t < 0 || t >= N) continue;
if (!isOpen(t, cur.d)) continue;
const nd = enter(t, cur.d);
const key = encode(t, nd);
if (!seen.has(key)){ seen.add(key); prev.set(key, encode(cur.p, cur.d)); queue.push({ p: t, d: nd }); }
}
}
const total = N * (1 << NDOORS);
if (goal){
lastSolution = goal;
pos = goal.p; doors = goal.d; render();
setStatus(fmt('Reachable! BFS explored %explored% of %total% configurations.', { explored: explored, total: total }), 'ok');
console.log('Flag reachable. Explored', explored, 'of', total, 'configurations.');
} else {
setStatus('The flag is NOT reachable from the start — no sequence of moves works.', 'bad');
console.log('Flag unreachable. Explored', explored, 'of', total, 'configurations.');
}
}
document.getElementById('left').addEventListener('click', () => step(-1));
document.getElementById('right').addEventListener('click', () => step(1));
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => { pos = START; doors = INIT.slice(); render(); setStatus('', ''); });
render();
Notice the catch. A single screen with a few switches has many possible configurations — Mario's position times every on/off combination of the doors. The computer can find a solution by exploring this whole graph of configurations, but its size doubles with every switch you add. Checking a finished walkthrough is easy; searching for one in a level with hundreds of switches is where things blow up.
Comments
Loading comments...