Chotto.app
Login
Sign Up
ES
App Name
kuyawa / solitaire
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%"> <!-- Vibrant Green App Icon Background --> <rect width="512" height="512" rx="112" ry="112" fill="#10B981" /> <!-- Main Group - Tilted 30 degrees around the exact center --> <g transform="rotate(30, 256, 256)"> <!-- Clean Flat Card Base --> <rect x="136" y="88" width="240" height="336" rx="16" ry="16" fill="#FFFFFF" /> <!-- Subtle Inner Border Line --> <rect x="144" y="96" width="224" height="320" rx="10" ry="10" fill="none" stroke="#F3F4F6" stroke-width="2" /> <!-- Center Spade Graphic --> <path d="M256,180 C256,180 190,260 190,290 A35,35 0 0,0 256,315 A35,35 0 0,0 322,290 C322,260 256,180 256,180 Z M248,310 L230,350 L282,350 L264,310 Z" fill="#111827" /> <!-- Top Left Corner --> <g> <text x="168" y="138" font-family="Arial, Helvetica, sans-serif" font-size="32" font-weight="bold" text-anchor="middle" fill="#111827">A</text> <!-- Mini Top Left Spade --> <path d="M168,148 C168,148 152,165 152,171 A8,8 0 0,0 168,177 A8,8 0 0,0 184,171 C184,165 168,148 168,148 Z M166,175 L158,185 L178,185 L170,175 Z" fill="#111827" /> </g> <!-- Bottom Right Corner (Symmetrically inverted using rotation) --> <g transform="rotate(180, 256, 256)"> <text x="168" y="138" font-family="Arial, Helvetica, sans-serif" font-size="32" font-weight="bold" text-anchor="middle" fill="#111827">A</text> <!-- Mini Bottom Right Spade --> <path d="M168,148 C168,148 152,165 152,171 A8,8 0 0,0 168,177 A8,8 0 0,0 184,171 C184,165 168,148 168,148 Z M166,175 L158,185 L178,185 L170,175 Z" fill="#111827" /> </g> </g> </svg>
HTML Content
<!DOCTYPE html> <html lang="en" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"> <title>Solitaire</title> <style> :root{--sp:0.2s} [data-theme="dark"]{ --bg:#0a100d;--bg2:#0f1a14;--sf:#141e18;--bd:#1e3028; --fg:#d8d4cc;--mt:#5a6b60;--ac:#d4a843;--ach:#e4b853; --cb:#f0ece2;--cs:rgba(0,0,0,0.35);--rs:#d44040;--bs:#1a1a1a; --bbg:#1a3828;--bbd:#2a5838;--bp:rgba(212,168,67,0.12); --ph:rgba(212,168,67,0.08);--ov:rgba(6,10,8,0.88); --ds:rgba(0,0,0,0.55);--bt:#1a2e22;--bth:#243a2e; --g1:rgba(30,90,50,0.07);--g2:rgba(30,90,50,0.05); } [data-theme="light"]{ --bg:#1a6838;--bg2:#166030;--sf:rgba(0,0,0,0.1);--bd:rgba(0,0,0,0.15); --fg:#f0ece4;--mt:rgba(255,255,255,0.45);--ac:#c49530;--ach:#d4a540; --cb:#ffffff;--cs:rgba(0,0,0,0.22);--rs:#c03030;--bs:#1a1a2a; --bbg:#1a3a5a;--bbd:#3a6a8a;--bp:rgba(255,255,255,0.1); --ph:rgba(255,255,255,0.1);--ov:rgba(10,48,26,0.9); --ds:rgba(0,0,0,0.35);--bt:rgba(0,0,0,0.15);--bth:rgba(0,0,0,0.22); --g1:rgba(255,255,255,0.03);--g2:rgba(255,255,255,0.02); } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} body{ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif; background:var(--bg);color:var(--fg);min-height:100vh; display:flex;flex-direction:column;overflow-x:hidden;transition:background var(--sp); } body::before{ content:'';position:fixed;inset:0;pointer-events:none;z-index:0; background:radial-gradient(ellipse 70% 50% at 20% 10%,var(--g1),transparent), radial-gradient(ellipse 60% 60% at 80% 90%,var(--g2),transparent); } header{ position:relative;z-index:2;width:100%;max-width:800px;margin:0 auto; padding:12px 16px;display:flex;align-items:center;justify-content:space-between; } .logo{font-size:18px;font-weight:800;letter-spacing:-0.4px} .logo span{color:var(--ac)} .hm{display:flex;gap:14px;align-items:center} .st{font-size:12px;font-weight:600;color:var(--mt);font-variant-numeric:tabular-nums} .ib{ width:36px;height:36px;border:1px solid var(--bd);border-radius:9px; background:var(--bt);color:var(--fg);cursor:pointer;font-size:15px; display:flex;align-items:center;justify-content:center;transition:all var(--sp); } .ib:hover{border-color:var(--ac);background:var(--bth)} .ib:focus-visible,.ab:focus-visible,.wb:focus-visible{outline:2px solid var(--ac);outline-offset:2px} main{position:relative;z-index:1;width:100%;max-width:800px;margin:0 auto;padding:0 8px 24px;flex:1} #game{width:100%;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none} #topR{display:grid;grid-template-columns:repeat(7,var(--cw));gap:var(--g);justify-content:center} .ps{width:var(--cw);height:var(--ch);position:relative;border-radius:calc(var(--cw)*0.06);flex-shrink:0} .ps.sp{border:none;background:none} .ps.fd{border:1.5px dashed var(--bd);background:var(--ph)} .ps.fd::after{ content:'\25C6';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); font-size:var(--fl);color:var(--mt);opacity:0.35;pointer-events:none; } .ps.fd.hc::after{display:none} #stock{ border:1.5px dashed var(--bd);background:var(--ph);cursor:pointer; transition:transform 0.15s; } #stock:hover{transform:scale(1.03)} #stock.hc{border:none;background:none} #stock.es{cursor:pointer} #stock.es::after{ content:'\21BB';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); font-size:var(--fl);color:var(--mt);opacity:0.6;pointer-events:none; /*animation:rspin 2s ease-in-out infinite;*/ } @keyframes rspin{ 0%,100%{transform:translate(-50%,-50%) rotate(0deg)} 50%{transform:translate(-50%,-50%) rotate(360deg)} } .stock-back{ position:absolute;top:0;left:0;width:100%;height:100%; border-radius:calc(var(--cw)*0.06);background:var(--bbg); box-shadow:0 1px 3px var(--cs);overflow:hidden;pointer-events:none; } .stock-bi{ position:absolute;inset:3px;border-radius:calc(var(--cw)*0.04); border:1.5px solid var(--bbd); background-image:repeating-linear-gradient(45deg,transparent,transparent 5px,var(--bp) 5px,var(--bp) 6px), repeating-linear-gradient(-45deg,transparent,transparent 5px,var(--bp) 5px,var(--bp) 6px); } .stock-ct{ position:absolute;bottom:3px;right:5px;font-size:calc(var(--fs)*0.8); font-weight:700;color:var(--mt);opacity:0.5;pointer-events:none;z-index:2; } #tabR{display:grid;grid-template-columns:repeat(7,var(--cw));gap:var(--g);justify-content:center;margin-top:14px} .tc{min-height:var(--ch)} .dh{box-shadow:0 0 0 2px var(--ac),0 0 14px rgba(212,168,67,0.25)!important} .card{ position:absolute;width:var(--cw);height:var(--ch); border-radius:calc(var(--cw)*0.06);cursor:pointer; transition:transform 0.1s,box-shadow 0.1s;will-change:transform; } .card.fu{background:var(--cb);box-shadow:0 1px 3px var(--cs),inset 0 1px 0 rgba(255,255,255,0.4)} .card.fd{background:var(--bbg);box-shadow:0 1px 3px var(--cs)} .card.fu:hover:not(.dg){transform:translateY(-2px);box-shadow:0 4px 10px var(--cs)} .card.dg{box-shadow:0 10px 28px var(--ds)!important;transform:rotate(2deg)!important;cursor:grabbing;transition:none!important;z-index:9999!important} .cf{display:none;position:absolute;inset:0;padding:3px 4px;overflow:hidden} .card.fu .cf{display:block} .cback{display:none;position:absolute;inset:0;border-radius:inherit;overflow:hidden} .card.fd .cback{display:flex} .cbi{ position:absolute;inset:3px;border-radius:calc(var(--cw)*0.04);border:1.5px solid var(--bbd); background-image:repeating-linear-gradient(45deg,transparent,transparent 5px,var(--bp) 5px,var(--bp) 6px), repeating-linear-gradient(-45deg,transparent,transparent 5px,var(--bp) 5px,var(--bp) 6px); } .tl,.br{position:absolute;display:flex;flex-direction:column;align-items:center;line-height:1;font-weight:700} .tl{top:2px;left:3px}.br{bottom:2px;right:3px;transform:rotate(180deg)} .cv{font-size:var(--fs)}.cs2{font-size:calc(var(--fs)*0.85);margin-top:-1px} .cm{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:var(--fl);line-height:1;pointer-events:none} .cf.r{color:var(--rs)}.cf.b{color:var(--bs)} .card.foundation-flash{animation:fflash 0.35s ease-out} @keyframes fflash{ 0%{box-shadow:0 0 0 0 rgba(212,168,67,0.6)} 50%{box-shadow:0 0 20px 6px rgba(212,168,67,0.3)} 100%{box-shadow:0 1px 3px var(--cs),inset 0 1px 0 rgba(255,255,255,0.4)} } #dragL{position:fixed;inset:0;pointer-events:none;z-index:1000} #bb{display:flex;gap:8px;margin-top:16px;justify-content:center;flex-wrap:wrap} .ab{ padding:8px 20px;border:1px solid var(--bd);border-radius:9px; background:var(--bt);color:var(--fg);font-size:12.5px;font-weight:600; cursor:pointer;transition:all var(--sp);font-family:inherit; } .ab:hover{border-color:var(--ac);background:var(--bth)} .ab:disabled{opacity:0.35;cursor:default;pointer-events:none} #winO{ position:fixed;inset:0;background:var(--ov);display:flex;align-items:center; justify-content:center;z-index:2000;opacity:0;pointer-events:none;transition:opacity 0.35s; backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px); } #winO.sh{opacity:1;pointer-events:all} .wc{ background:var(--sf);border:1px solid var(--bd);border-radius:18px; padding:36px 32px;text-align:center;box-shadow:0 12px 40px rgba(0,0,0,0.3); transform:scale(0.88) translateY(10px);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1); max-width:320px;width:88%; } #winO.sh .wc{transform:scale(1) translateY(0)} .wi{font-size:42px;margin-bottom:12px;display:block;line-height:1;color:var(--ac)} .wt{font-size:24px;font-weight:800;margin-bottom:6px;letter-spacing:-0.5px} .ws{color:var(--mt);font-size:13.5px;margin-bottom:24px;line-height:1.6} .wb{ padding:11px 32px;border:none;border-radius:10px;background:var(--ac);color:#000; font-size:13.5px;font-weight:700;cursor:pointer;transition:all var(--sp);font-family:inherit; } .wb:hover{background:var(--ach);transform:translateY(-2px);box-shadow:0 4px 14px rgba(212,168,67,0.3)} ::-webkit-scrollbar{width:5px;height:5px} ::-webkit-scrollbar-track{background:var(--bg2);border-radius:3px} ::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px} @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;transition-duration:0.01ms!important}} @media(max-width:480px){header{padding:10px 10px}main{padding:0 4px 20px}#tabR{margin-top:10px}.ab{padding:7px 14px;font-size:11.5px}} </style> </head> <body> <header> <div class="logo"><span>♠</span> Solitaire</div> <div class="hm"> <span class="st" id="mSt">0 moves</span> <span class="st" id="tSt">0:00</span> </div> <button class="ib" id="thB" aria-label="Toggle theme">☼</button> </header> <main> <div id="game"> <div id="topR"> <div class="ps" id="stock" role="button" aria-label="Stock pile" tabindex="0"></div> <div class="ps" id="waste"></div> <div class="ps sp"></div> <div class="ps fd" id="f0"></div> <div class="ps fd" id="f1"></div> <div class="ps fd" id="f2"></div> <div class="ps fd" id="f3"></div> </div> <div id="tabR"></div> </div> <div id="bb"> <button class="ab" id="newB">New Game</button> <button class="ab" id="autB" disabled>Auto Complete</button> </div> </main> <div id="dragL"></div> <div id="winO"> <div class="wc"> <span class="wi">♦</span> <div class="wt">You Win</div> <div class="ws" id="wSu"></div> <button class="wb" id="wBu">Play Again</button> </div> </div> <script> (function(){ var SUITS=['spades','hearts','diamonds','clubs']; var SYM={spades:'\u2660',hearts:'\u2665',diamonds:'\u2666',clubs:'\u2663'}; var RED=new Set(['hearts','diamonds']); var VALS=['A','2','3','4','5','6','7','8','9','10','J','Q','K']; var allCards=[],stock=[],waste=[],fnd=[[],[],[],[]],tab=[[],[],[],[],[],[],[]]; var moves=0,secs=0,tmr=null,started=false,won=false,autoRun=false; var cW,cH,fdO,fuO,gp; /* Drag state */ var isDrag=false,dCards=[],dSrc=null,dOX=0,dOY=0; var dLifted=false; /* true once cards are visually lifted to drag layer */ /* Touch tracking */ var tStartPos=null,tMoved=false; /* Double-tap tracking for touch */ var lastTapTime=0,lastTapCard=null; /* DOM */ var stockEl=document.getElementById('stock'); var wasteEl=document.getElementById('waste'); var fEl=[document.getElementById('f0'),document.getElementById('f1'),document.getElementById('f2'),document.getElementById('f3')]; var tabR=document.getElementById('tabR'); var dragL=document.getElementById('dragL'); var mSt=document.getElementById('mSt'); var tSt=document.getElementById('tSt'); var thB=document.getElementById('thB'); var newB=document.getElementById('newB'); var autB=document.getElementById('autB'); var winO=document.getElementById('winO'); var wSu=document.getElementById('wSu'); var wBu=document.getElementById('wBu'); var tEl=[]; for(var i=0;i<7;i++){ var c=document.createElement('div');c.className='ps tc';c.id='t'+i; tabR.appendChild(c);tEl.push(c); } /* ======== Theme ======== */ function setTh(t){ document.documentElement.setAttribute('data-theme',t); thB.innerHTML=t==='dark'?'\u263C':'\u263E'; try{localStorage.setItem('sol-th',t)}catch(e){} } thB.onclick=function(){setTh(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark')}; try{var sv=localStorage.getItem('sol-th');if(sv)setTh(sv);else setTh('dark')}catch(e){setTh('dark')} /* ======== Sizing ======== */ function upSz(){ var vw=window.innerWidth,pad=Math.max(12,vw*0.025); gp=Math.max(4,Math.min(10,Math.round(vw*0.009))); cW=Math.floor((vw-pad*2-gp*6)/7); cW=Math.min(cW,100);cW=Math.max(cW,36); cH=Math.round(cW*1.4); fdO=Math.max(2,Math.round(cW*0.07)); fuO=Math.max(10,Math.round(cW*0.28)); var r=document.documentElement.style; r.setProperty('--cw',cW+'px');r.setProperty('--ch',cH+'px'); r.setProperty('--g',gp+'px'); r.setProperty('--fs',Math.round(cW*0.22)+'px'); r.setProperty('--fl',Math.round(cW*0.42)+'px'); } /* ======== Timer ======== */ function startTmr(){if(tmr)return;tmr=setInterval(function(){secs++;tSt.textContent=fmt(secs)},1000)} function stopTmr(){clearInterval(tmr);tmr=null} function resetTmr(){stopTmr();secs=0;tSt.textContent='0:00'} function fmt(s){var m=Math.floor(s/60);return m+':'+(s%60<10?'0':'')+s%60} /* ======== Card Creation ======== */ function mkCard(suit,val,vidx){ var card={suit:suit,val:val,vidx:vidx,faceUp:false,el:null,src:null}; var el=document.createElement('div'); el.className='card fd';el.tabIndex=0; el.setAttribute('role','button'); el.setAttribute('aria-label',val+' of '+suit); var back=document.createElement('div');back.className='cback'; var bi=document.createElement('div');bi.className='cbi'; back.appendChild(bi);el.appendChild(back); var face=document.createElement('div'); face.className='cf '+(RED.has(suit)?'r':'b'); var tl=document.createElement('div');tl.className='tl'; tl.innerHTML='<span class="cv">'+val+'</span><span class="cs2">'+SYM[suit]+'</span>'; var br=document.createElement('div');br.className='br'; br.innerHTML='<span class="cv">'+val+'</span><span class="cs2">'+SYM[suit]+'</span>'; var mid=document.createElement('div');mid.className='cm';mid.textContent=SYM[suit]; face.appendChild(tl);face.appendChild(br);face.appendChild(mid); el.appendChild(face); card.el=el; bindCard(card); return card; } /* ======== Card Pointer Events ======== */ function bindCard(card){ var el=card.el; /* Mouse */ el.addEventListener('mousedown',function(e){ e.preventDefault(); onDown(card,e.clientX,e.clientY,false); }); /* Touch */ el.addEventListener('touchstart',function(e){ var t=e.touches[0]; tStartPos={x:t.clientX,y:t.clientY}; tMoved=false; onDown(card,t.clientX,t.clientY,true); },{passive:true}); } /* Mouse move/up */ document.addEventListener('mousemove',function(e){if(isDrag)onMove(e.clientX,e.clientY)}); document.addEventListener('mouseup',function(e){if(isDrag)onUp(e.clientX,e.clientY,false,false)}); /* Touch move/end/cancel */ document.addEventListener('touchmove',function(e){ if(!isDrag)return; var t=e.touches[0]; if(tStartPos){ var dx=t.clientX-tStartPos.x,dy=t.clientY-tStartPos.y; if(Math.abs(dx)+Math.abs(dy)>6)tMoved=true; } onMove(t.clientX,t.clientY); if(tMoved)e.preventDefault(); },{passive:false}); document.addEventListener('touchend',function(e){ if(!isDrag)return; var t=e.changedTouches[0]; onUp(t.clientX,t.clientY,true,!tMoved); tStartPos=null; }); document.addEventListener('touchcancel',function(){if(isDrag)cancelDrag()}); /* ======== Stock Click ======== */ stockEl.addEventListener('click',function(e){ e.preventDefault();e.stopPropagation(); if(won||autoRun||isDrag)return; if(!started){started=true;startTmr()} if(stock.length>0){ var card=stock.pop(); card.faceUp=true;card.src='waste'; flipEl(card);waste.push(card); moves++;mSt.textContent=moves+' move'+(moves!==1?'s':''); }else if(waste.length>0){ while(waste.length>0){ var c=waste.pop(); c.faceUp=false;c.src='stock'; flipEl(c);stock.push(c); } } render(); }); stockEl.addEventListener('keydown',function(e){ if(e.key==='Enter'||e.key===' '){e.preventDefault();stockEl.click()} }); /* Prevent context menu on game area */ document.getElementById('game').addEventListener('contextmenu',function(e){e.preventDefault()}); /* ======== Drag System ======== */ function onDown(card,x,y,isTouch){ if(won||autoRun)return; if(!card.faceUp)return; var src=findSrc(card); if(!src)return; var cards=getDragCards(card,src); if(!cards||cards.length===0)return; var rect=card.el.getBoundingClientRect(); dOX=x-rect.left;dOY=y-rect.top; dCards=cards;dSrc=src; isDrag=true;dLifted=false;tMoved=false; /* For mouse: lift immediately. For touch: defer until movement detected. */ if(!isTouch){ liftCards(x,y); } } function liftCards(x,y){ if(dLifted)return; dLifted=true; for(var i=0;i<dCards.length;i++){ dCards[i].el.classList.add('dg'); dragL.appendChild(dCards[i].el); dCards[i].el.style.left=(x-dOX)+'px'; dCards[i].el.style.top=(y-dOY+i*fuO)+'px'; dCards[i].el.style.zIndex=1000+i; } render(); showHints(dCards,dSrc); } function onMove(x,y){ /* For touch: lift cards on first real movement */ if(!dLifted){ liftCards(x,y); } for(var i=0;i<dCards.length;i++){ dCards[i].el.style.left=(x-dOX)+'px'; dCards[i].el.style.top=(y-dOY+i*fuO)+'px'; } } function onUp(x,y,isTouch,wasTap){ if(!isDrag)return; clearHints(); var moved=false; var target=findDrop(x,y); if(dLifted&&target&&canDrop(dCards,target)){ doMove(dSrc,target,dCards); moves++;mSt.textContent=moves+' move'+(moves!==1?'s':''); if(!started){started=true;startTmr()} moved=true; } /* Return cards to their piles visually */ for(var i=0;i<dCards.length;i++)dCards[i].el.classList.remove('dg'); /* Save references before clearing state */ var tapCard=wasTap&&!moved&&dCards.length===1?dCards[0]:null; isDrag=false;dCards=[];dSrc=null;dLifted=false; render(); if(moved){checkAuto();checkWin();return} /* Double-tap detection (touch only, mouse uses native dblclick) */ if(isTouch&&tapCard){ var now=Date.now(); if(now-lastTapTime<380&&lastTapCard===tapCard){ tryAutoFoundation(tapCard); lastTapTime=0;lastTapCard=null; }else{ lastTapTime=now;lastTapCard=tapCard; } } } function cancelDrag(){ clearHints(); for(var i=0;i<dCards.length;i++)dCards[i].el.classList.remove('dg'); isDrag=false;dCards=[];dSrc=null;dLifted=false; render(); } /* ======== Double-click to foundation (mouse) ======== */ document.getElementById('game').addEventListener('dblclick',function(e){ console.log('DBLCLICK') if(won||autoRun)return; var el=e.target.closest('.card.fu'); if(!el)return; var card=findCardByEl(el); if(!card)return; tryAutoFoundation(card); }); /* ======== Auto-move to foundation ======== */ function tryAutoFoundation(card){ if(won||autoRun)return; var src=findSrc(card); if(!src)return; var dc=getDragCards(card,src); if(!dc||dc.length!==1)return; for(var i=0;i<4;i++){ if(canDrop(dc,{type:'fnd',idx:i})){ doMove(src,{type:'fnd',idx:i},dc); moves++;mSt.textContent=moves+' move'+(moves!==1?'s':''); if(!started){started=true;startTmr()} render(); /* Flash animation on the placed card */ var placed=fnd[i][fnd[i].length-1]; if(placed&&placed.el){ placed.el.classList.add('foundation-flash'); setTimeout(function(){if(placed.el)placed.el.classList.remove('foundation-flash')},400); } checkAuto();checkWin(); return true; } } return false; } /* ======== Source / Drag Helpers ======== */ function findSrc(card){ if(card.src==='waste')return{type:'waste',idx:0}; for(var i=0;i<4;i++)if(fnd[i].length>0&&fnd[i][fnd[i].length-1]===card)return{type:'fnd',idx:i}; for(var i=0;i<7;i++){ var pi=tab[i].indexOf(card); if(pi!==-1)return{type:'tab',idx:i,ci:pi}; } return null; } function getDragCards(card,src){ if(src.type==='waste')return waste.length>0?[waste[waste.length-1]]:null; if(src.type==='fnd')return fnd[src.idx].length>0?[fnd[src.idx][fnd[src.idx].length-1]]:null; if(src.type==='tab'){ var ci=src.ci; var cards=tab[src.idx].slice(ci); for(var i=0;i<cards.length;i++)if(!cards[i].faceUp)return null; return cards; } return null; } function findDrop(x,y){ for(var i=0;i<4;i++){ var r=fEl[i].getBoundingClientRect(); if(x>=r.left&&x<=r.right&&y>=r.top&&y<=r.bottom)return{type:'fnd',idx:i}; } for(var i=0;i<7;i++){ var col=tab[i]; var rect=tEl[i].getBoundingClientRect(); if(x<rect.left||x>rect.right)continue; if(col.length===0){ if(y>=rect.top&&y<=rect.bottom)return{type:'tab',idx:i}; }else{ var lr=col[col.length-1].el.getBoundingClientRect(); if(y>=rect.top&&y<=lr.bottom+4)return{type:'tab',idx:i}; } } return null; } function canDrop(cards,target){ var first=cards[0]; if(target.type==='fnd'){ if(cards.length!==1)return false; var pile=fnd[target.idx]; if(pile.length===0)return first.vidx===0; var top=pile[pile.length-1]; return first.suit===top.suit&&first.vidx===top.vidx+1; } if(target.type==='tab'){ var col=tab[target.idx]; if(col.length===0)return first.vidx===12; var top2=col[col.length-1]; if(!top2.faceUp)return false; return RED.has(first.suit)!==RED.has(top2.suit)&&first.vidx===top2.vidx-1; } return false; } function doMove(src,tgt,cards){ if(src.type==='waste')waste.pop(); else if(src.type==='fnd')fnd[src.idx].pop(); else if(src.type==='tab')tab[src.idx].splice(src.ci,cards.length); if(tgt.type==='fnd'){ fnd[tgt.idx].push(cards[0]);cards[0].src='fnd'; }else if(tgt.type==='tab'){ for(var i=0;i<cards.length;i++){tab[tgt.idx].push(cards[i]);cards[i].src='tab'} } /* Auto-flip top of source tableau */ if(src.type==='tab'){ var col=tab[src.idx]; if(col.length>0&&!col[col.length-1].faceUp){ col[col.length-1].faceUp=true; flipEl(col[col.length-1]); } } } function findCardByEl(el){ for(var i=0;i<allCards.length;i++)if(allCards[i].el===el)return allCards[i]; return null; } function flipEl(card){ if(card.faceUp){card.el.classList.remove('fd');card.el.classList.add('fu')} else{card.el.classList.remove('fu');card.el.classList.add('fd')} } /* ======== Drop Hints ======== */ function showHints(cards){ clearHints(); for(var i=0;i<4;i++)if(canDrop(cards,{type:'fnd',idx:i}))fEl[i].classList.add('dh'); for(var i=0;i<7;i++)if(canDrop(cards,{type:'tab',idx:i}))tEl[i].classList.add('dh'); } function clearHints(){ for(var i=0;i<4;i++)fEl[i].classList.remove('dh'); for(var i=0;i<7;i++)tEl[i].classList.remove('dh'); } /* ======== Render ======== */ function render(){ /* Stock */ stockEl.innerHTML=''; stockEl.classList.remove('hc','es'); if(stock.length>0){ stockEl.classList.add('hc'); var sb=document.createElement('div');sb.className='stock-back'; var sbi=document.createElement('div');sbi.className='stock-bi'; sb.appendChild(sbi);stockEl.appendChild(sb); if(stock.length>1){ var sc=document.createElement('span');sc.className='stock-ct'; sc.textContent=stock.length;stockEl.appendChild(sc); } stockEl.setAttribute('aria-label','Stock: '+stock.length+' cards'); }else if(waste.length>0){ stockEl.classList.add('es'); stockEl.setAttribute('aria-label','Stock empty \u2014 click to recycle'); }else{ stockEl.setAttribute('aria-label','Stock empty'); } /* Waste */ wasteEl.innerHTML=''; if(waste.length>0){ var wc=waste[waste.length-1]; if(!isDrag||dSrc.type!=='waste'){ wasteEl.appendChild(wc.el); wc.el.style.left='0px';wc.el.style.top='0px';wc.el.style.zIndex=''; wc.el.classList.remove('dg'); } } /* Foundations */ for(var i=0;i<4;i++){ fEl[i].classList.toggle('hc',fnd[i].length>0); fEl[i].innerHTML=''; if(fnd[i].length>0){ var fc=fnd[i][fnd[i].length-1]; if(!isDrag||dSrc.type!=='fnd'||dSrc.idx!==i){ fEl[i].appendChild(fc.el); fc.el.style.left='0px';fc.el.style.top='0px';fc.el.style.zIndex=''; fc.el.classList.remove('dg'); } } } /* Tableau */ for(var i=0;i<7;i++){ tEl[i].innerHTML=''; var col=tab[i],y=0; for(var j=0;j<col.length;j++){ var card=col[j]; if(isDrag&&dSrc.type==='tab'&&dSrc.idx===i&&j>=dSrc.ci)continue; tEl[i].appendChild(card.el); card.el.style.left='0px'; card.el.style.top=y+'px'; card.el.style.zIndex=j+''; card.el.classList.remove('dg'); y+=card.faceUp?fuO:fdO; } tEl[i].style.minHeight=(col.length>0?y+cH:cH)+'px'; } } /* ======== Auto-Complete ======== */ function checkAuto(){ if(won)return; var ok=true; for(var i=0;i<7;i++){ for(var j=0;j<tab[i].length;j++){ if(!tab[i][j].faceUp){ok=false;break} } if(!ok)break; } autB.disabled=!ok; } function autoComplete(){ if(won||autoRun)return; autoRun=true;autB.disabled=true; doAutoStep(); } function doAutoStep(){ if(won){autoRun=false;return} var moved=false; if(waste.length>0){ var wc=waste[waste.length-1]; for(var i=0;i<4;i++){ if(canDrop([wc],{type:'fnd',idx:i})){ doMove({type:'waste',idx:0},{type:'fnd',idx:i},[wc]); moves++;mSt.textContent=moves+' move'+(moves!==1?'s':''); moved=true;break; } } } if(!moved){ for(var i=0;i<7;i++){ if(tab[i].length===0)continue; var tc=tab[i][tab[i].length-1]; for(var j=0;j<4;j++){ if(canDrop([tc],{type:'fnd',idx:j})){ doMove({type:'tab',idx:i,ci:tab[i].length-1},{type:'fnd',idx:j},[tc]); moves++;mSt.textContent=moves+' move'+(moves!==1?'s':''); moved=true;break; } } if(moved)break; } } render(); if(moved&&!won){setTimeout(doAutoStep,80)} else{autoRun=false;checkWin()} } /* ======== Win Check ======== */ function checkWin(){ var total=fnd[0].length+fnd[1].length+fnd[2].length+fnd[3].length; if(total===52){ won=true;stopTmr(); setTimeout(function(){ wSu.textContent='Completed in '+fmt(secs)+' with '+moves+' moves.'; winO.classList.add('sh'); },300); } } /* ======== New Game ======== */ function newGame(){ won=false;autoRun=false;moves=0;started=false; isDrag=false;dCards=[];dSrc=null;dLifted=false; lastTapTime=0;lastTapCard=null; resetTmr();mSt.textContent='0 moves';autB.disabled=true; winO.classList.remove('sh'); stockEl.innerHTML='';wasteEl.innerHTML=''; for(var i=0;i<4;i++){fnd[i]=[];fEl[i].innerHTML='';fEl[i].classList.remove('hc')} for(var i=0;i<7;i++){tab[i]=[];tEl[i].innerHTML=''} dragL.innerHTML=''; allCards=[];stock=[];waste=[]; for(var s=0;s<4;s++)for(var v=0;v<13;v++){ allCards.push(mkCard(SUITS[s],VALS[v],v)); } for(var i=allCards.length-1;i>0;i--){ var j=Math.floor(Math.random()*(i+1)); var tmp=allCards[i];allCards[i]=allCards[j];allCards[j]=tmp; } var idx=0; for(var col=0;col<7;col++){ for(var row=0;row<=col;row++){ var card=allCards[idx++]; card.faceUp=(row===col);card.src='tab'; flipEl(card);tab[col].push(card); } } for(;idx<52;idx++){ var card=allCards[idx]; card.faceUp=false;card.src='stock'; stock.push(card); } render(); } /* ======== Events ======== */ newB.onclick=newGame; autB.onclick=autoComplete; wBu.onclick=function(){winO.classList.remove('sh');newGame()}; document.addEventListener('keydown',function(e){ if(e.key==='n'||e.key==='N')newGame(); if(e.key==='Escape')winO.classList.remove('sh'); }); var rto; window.addEventListener('resize',function(){ clearTimeout(rto);rto=setTimeout(function(){upSz();render()},100); }); /* ======== Init ======== */ upSz();newGame(); })(); </script> </body> </html>
Cancel