Every move keeps all104 cards

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html>
<html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Every move keeps all104 cards</title><style>
:root{color-scheme:light;--accent:#315846;--wash:#e7f2e8;--ink:#202d3b;--paper:#fffdf9}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:#eeeae2;font:17px/1.65 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:980px;margin:auto;padding:28px 24px 40px}header{border-top:6px solid var(--accent);padding:20px 0 24px}.kicker{color:var(--accent);font-weight:750;letter-spacing:.06em;margin:0 0 8px}h1{font:700 clamp(30px,5vw,46px)/1.15 Georgia,serif;margin:0 0 14px;max-width:780px}h2{font-size:23px;line-height:1.3;margin:0 0 14px}h3{font-size:19px;margin:12px 0 8px}p{margin:0 0 16px}button,input,textarea,select{font:inherit}button{background:white;border:1px solid #87949b;color:var(--ink);border-radius:8px;padding:11px 16px;min-height:48px;cursor:pointer;font-weight:650}button:hover{background:var(--wash)}button[aria-pressed=true],button.primary{background:var(--accent);color:white;border-color:var(--accent)}:focus-visible{outline:3px solid #b45a1e;outline-offset:3px}label{display:block;font-weight:650;margin:14px 0 6px}input:not([type=checkbox]):not([type=radio]),textarea,select{display:block;width:100%;min-width:0;max-width:100%;border:1px solid #80909b;border-radius:7px;padding:11px;background:white;color:var(--ink)}input[type=checkbox],input[type=radio]{width:21px;height:21px;accent-color:var(--accent)}textarea{resize:vertical}.panel{background:var(--paper);border:1px solid #ccd2d1;border-radius:12px;padding:24px;margin-bottom:20px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.two>.panel{margin:0}.two{margin-bottom:20px}.actions{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0}.result{padding:18px;background:var(--wash);border-left:5px solid var(--accent);margin-top:18px;white-space:pre-line;overflow-wrap:anywhere}.note{color:#465565;font-size:16px}.strip{padding:18px;background:var(--wash);border-radius:8px;margin-bottom:18px}table{border-collapse:collapse;width:100%;font-size:16px;table-layout:fixed}th,td{text-align:left;padding:10px 8px;border-bottom:1px solid #cbd0d1;overflow-wrap:anywhere;vertical-align:top}th{background:var(--wash)}ol,ul{padding-left:24px}li+li{margin-top:8px}pre{font-size:16px;white-space:pre-wrap;overflow-wrap:anywhere}code{font-size:16px}.grid{display:grid;gap:5px;max-width:440px}.grid button{padding:5px;min-width:0}.grid input{padding:5px!important;text-align:center;font-weight:750}blockquote{font:500 26px/1.5 Georgia,serif;margin:16px 0}footer{font-size:16px;color:#465565;margin-top:22px}[hidden]{display:none!important}svg{display:block;max-width:100%;height:auto}mark{background:#e0efad;color:var(--ink)}@media(max-width:560px){main{padding:18px 12px 28px}.panel{padding:20px 16px}.two{grid-template-columns:minmax(0,1fr)}h1{font-size:32px}.actions button{flex:1 1 135px}th,td{padding:8px 5px}.grid button{min-height:40px}blockquote{font-size:24px}}
.columns{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.column{border:1px solid #a9bdb1;border-radius:9px;padding:14px;background:#f9fff8}.cards{display:flex;gap:6px;flex-wrap:wrap;margin:12px 0}.cards button{font-size:16px;padding:8px;min-height:44px}@media(max-width:560px){.columns{grid-template-columns:minmax(0,1fr)}}</style></head><body><main><header><p class="kicker">TWO-SUIT SPIDER · DETERMINISTIC CARD STATE</p><h1>Every move keeps all104 cards</h1><p>Select a valid same-suit descending face-up suffix, then choose a destination. Deterministic fixture; not promised winnable. Card records are inspectable.</p></header><div class="actions"><button type="button" id="deal">Deal ten stock cards</button><button type="button" id="undo">Undo last valid action</button><button type="button" id="reset">Reset exact deal</button></div><section class="panel"><h2>Card partition</h2><div id="summary" class="result" role="status" aria-live="polite"></div><div id="selection" class="result" role="status" aria-live="polite">No suffix selected.</div><div id="feedback" class="result" role="status" aria-live="polite">Select a face-up card.</div></section><section class="panel"><h2>Tableau columns 0–9</h2><p id="focusState" role="status">Tableau controls are ready.</p><div id="columns" class="columns"></div></section><section class="panel"><h2>Controlled completion inspections</h2><p>Separate staged states, not claimed reachable playthroughs. Stock is zero; unused IDs remain in the other columns.</p><div class="actions"><button type="button" id="stage1">Stage one complete run</button><button type="button" id="stage2">Stage two stacked runs</button><button type="button" id="resolve">Resolve staged top runs</button></div><div id="completed" class="result" role="status" aria-live="polite">No stored completed cards.</div></section><footer>A movable group must share a suit; its destination only needs the next-higher rank. Every card remains counted.</footer><noscript>JavaScript is required for the local controls. The accompanying article contains worked examples.</noscript></main><script>
(()=>{'use strict';const $=id=>document.getElementById(id);
let state,selected=null;const history=[];const copy=x=>JSON.parse(JSON.stringify(x));function deck(){return Array.from({length:104},(_,n)=>({id:Math.floor(n/13)+'-'+(n%13+1),rank:n%13+1,suit:Math.floor(n/13)%2?'Hearts':'Spades',up:false}));}function partition(){const cards=state.cols.flat().concat(state.stock,state.done.flat());return cards.length===104&&new Set(cards.map(c=>c.id)).size===104;}function counts(){return 'Tableau '+state.cols.flat().length+' / Stock '+state.stock.length+' / Completed cards '+state.done.flat().length+' ('+state.done.length+' runs) / Unique IDs '+new Set(state.cols.flat().concat(state.stock,state.done.flat()).map(c=>c.id)).size+' / '+state.phase;}function label(c){return ({1:'A',11:'J',12:'Q',13:'K'}[c.rank]||c.rank)+' '+c.suit+' ['+c.id+']';}function render(){const priorFocus=document.activeElement?.id,tableauFocused=!!document.activeElement&&$('columns').contains(document.activeElement);if(!partition())throw Error('104-ID partition broken');$('summary').textContent=counts();$('completed').textContent=state.done.length?state.done.map((r,i)=>'Run '+(i+1)+': '+r.map(c=>c.id).join(', ')).join(' / '):'No stored completed cards.';$('columns').replaceChildren();state.cols.forEach((col,ci)=>{const section=document.createElement('section');section.className='column';const h=document.createElement('h3');h.textContent='Column '+ci+' · '+col.length+' cards';section.append(h);const down=document.createElement('p');down.textContent=col.filter(c=>!c.up).length+' face-down cards';section.append(down);const cards=document.createElement('div');cards.className='cards';col.forEach((card,at)=>{if(!card.up)return;const b=document.createElement('button');b.type='button';b.id='card-'+card.id;b.textContent=label(card);b.addEventListener('click',()=>pick(ci,at));cards.append(b);});section.append(cards);const target=document.createElement('button');target.type='button';target.id='dest'+ci;target.textContent='Move selected suffix here';target.addEventListener('click',()=>move(ci));section.append(target);$('columns').append(section);});if(tableauFocused){const replacement=document.getElementById(priorFocus)||$('dest0');replacement.focus();$('focusState').textContent='Tableau keyboard focus: '+replacement.id;}}function feedback(s){$('feedback').textContent=s;}function clearSelection(){selected=null;$('selection').textContent='No suffix selected.';}function pick(ci,at){if(state.phase!=='Playing')return;const run=state.cols[ci].slice(at);if(!run.length||run.some(c=>!c.up)||run.some((c,i)=>i&&(c.suit!==run[0].suit||run[i-1].rank!==c.rank+1))){clearSelection();feedback('Invalid pickup: suffix must be face up, same suit and descending without gaps.');return;}selected={ci,at};$('selection').textContent='Selected column '+ci+' from '+label(run[0])+' through its top; '+run.length+' cards.';}function resolveRuns(){let changed;do{changed=false;for(const col of state.cols){if(col.length<13)continue;const r=col.slice(-13);if(r.every((c,i)=>c.up&&c.suit===r[0].suit&&c.rank===13-i)){state.done.push(col.splice(-13));if(col.length)col[col.length-1].up=true;changed=true;}}}while(changed);state.phase=state.done.length===8?'Won':'Playing';}function snapshot(){history.push(copy(state));}function move(dest){if(state.phase!=='Playing')return;if(!selected){feedback('Select a movable suffix first. No cards changed.');return;}const {ci,at}=selected,source=state.cols[ci],run=source.slice(at),target=state.cols[dest],top=target.at(-1);if(dest===ci||top&&(!top.up||top.rank!==run[0].rank+1)){feedback('Invalid destination: use another empty column or a face-up next-higher rank. No cards changed.');return;}snapshot();state.cols[dest].push(...source.splice(at));if(source.length)source.at(-1).up=true;resolveRuns();clearSelection();feedback('Legal move committed once; all104 IDs retained; card partition preserved.');render();}function reset(){const d=deck(),cols=[];for(let i=0;i<10;i++){const col=d.splice(0,i<4?6:5);col.at(-1).up=true;cols.push(col);}state={cols,stock:d,done:[],phase:'Playing',staged:false};history.length=0;clearSelection();feedback('Exact deterministic deal restored.');render();}$('deal').addEventListener('click',()=>{if(state.phase!=='Playing')return;if(state.cols.some(c=>!c.length)||state.stock.length<10){feedback('Deal blocked: ten nonempty columns and at least ten stock cards required. State unchanged.');return;}snapshot();for(let i=0;i<10;i++){const card=state.stock.shift();card.up=true;state.cols[i].push(card);}resolveRuns();clearSelection();feedback('One atomic batch: ten cards dealt, one per column.');render();});$('undo').addEventListener('click',()=>{if(!history.length){feedback('No valid action to undo.');return;}state=history.pop();clearSelection();feedback('Complete pre-action state restored, including face states and stored runs.');render();});$('reset').addEventListener('click',reset);function stage(n){const d=deck(),hidden=d.find(c=>c.id==='6-7'),take=set=>Array.from({length:13},(_,i)=>{const c=d.find(c=>c.id===set+'-'+(13-i));c.up=true;return c;}),col=[hidden,...(n===2?take(2):[]),...take(0)],used=new Set(col.map(c=>c.id)),cols=[col,...Array.from({length:9},()=>[])];d.filter(c=>!used.has(c.id)).forEach((card,i)=>cols[1+i%9].push(card));for(let i=1;i<10;i++)cols[i].at(-1).up=true;state={cols,stock:[],done:[],phase:'Playing',staged:true};history.length=0;clearSelection();feedback('Controlled '+n+'-run stage loaded. Resolve to inspect completed-card storage.');render();}$('stage1').addEventListener('click',()=>stage(1));$('stage2').addEventListener('click',()=>stage(2));$('resolve').addEventListener('click',()=>{if(!state.staged){feedback('Load a controlled completion stage first.');return;}const before=copy(state);resolveRuns();if(JSON.stringify(before)!==JSON.stringify(state))history.push(before);clearSelection();feedback('Staged completion detector applied; all104 IDs retained.');render();});reset();
})();
</script></body></html>