Chotto.app
Login
Sign Up
ES
App Name
kuyawa / mahjong
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" viewBox="0 0 70 70" version="1.1"> <!--rect width="70px" fill="#111" height="70px" rx="12" /--> <g transform="translate(-228 -524)"> <path d="m271.85 563.59c-0.33338-0.86665-0.60005-1.3666-0.8-1.5-0.20005-0.13331-0.70004-0.19998-1.5-0.2-0.40004 0.00002-2.3 0.26669-5.7 0.8l-0.3 0.3c1.1333 1.0667 2.3 2.0667 3.5 3 1.2 0.93335 2.3 1.7667 3.3 2.5 1.0666 0.66668 2.2 1.3334 3.4 2 1.2 0.60001 2.2 1.1334 3 1.6 0.86661 0.40001 1.9 0.83334 3.1 1.3 1.2666 0.40001 2.1666 0.73334 2.7 1 0.59994 0.20001 1.4999 0.43334 2.7 0.7 1.1999 0.26667 1.9666 0.46667 2.3 0.6 0.33327 0.0667 1.0666 0.23334 2.2 0.5 1.1999 0.20001 1.8333 0.30001 1.9 0.3 0.39993 0.0667 0.7666 0.40001 1.1 1-4.6667 1.2-7.9667 1.8-9.9 1.8h-0.6-0.5c-0.80006 0-1.7-0.2-2.7-0.6-1-0.46666-2.8-1.7667-5.4-3.9-2.6-2.2-5.8667-5.3-9.8-9.3-0.20004 0.20002-0.30004 0.33335-0.3 0.4v10.3c-0.00004 0.33334 0.0666 1.1333 0.2 2.4 0.1333 1.2 0.19996 1.9667 0.2 2.3-0.00004 2.2-0.60004 4.4333-1.8 6.7-1.9334-1.2667-2.9-2.7333-2.9-4.4-0.00003-0.13333 0.0333-0.36667 0.1-0.7 0.1333-0.4 0.29997-1.1333 0.5-2.2 0.26663-1.0667 0.5333-2.4 0.8-4l-0.1-8.7c-0.73337 0.60002-1.8334 1.5-3.3 2.7-1.4667 1.1334-2.6334 2.0333-3.5 2.7-0.86669 0.66668-1.9334 1.5-3.2 2.5-1.2667 0.93334-2.3334 1.7-3.2 2.3-0.80002 0.53334-1.7 1.1-2.7 1.7-0.93335 0.53334-1.7667 0.93334-2.5 1.2-0.66668 0.26667-1.2334 0.4-1.7 0.4h-2.6c1.0666-0.73333 2.5-1.7333 4.3-3 1.8666-1.2667 3.1666-2.1333 3.9-2.6 0.73331-0.53332 1.7333-1.2667 3-2.2 1.3333-0.93332 2.2666-1.6 2.8-2 0.53331-0.46665 1.2333-1.0666 2.1-1.8 0.9333-0.79999 1.6333-1.4666 2.1-2 0.46664-0.53332 0.96664-1.1333 1.5-1.8 0.5333-0.73331 1.0333-1.4666 1.5-2.2-0.13337-0.13331-0.4667-0.19998-1-0.2-1.3334 0.00002-2.3667 0.13335-3.1 0.4-0.73336 0.20002-1.1667 0.40002-1.3 0.6-0.0667 0.20002-0.16669 0.53335-0.3 1-0.80003 0.00002-1.5334-0.73331-2.2-2.2-0.60003-1.4666-1.4-3.4-2.4-5.8-1-2.4666-2.1667-4.4666-3.5-6v-1.6h2.7l1.4 0.7c0.0666 0.0667 0.43331 0.10003 1.1 0.1 1.3333 0.00003 4.8333-0.8333 10.5-2.5v-3.9c-0.40004 0.00004-1.3667 0.13337-2.9 0.4-1.5334 0.20004-2.5 0.30004-2.9 0.3-0.93336 0.00004-2.2667-0.43329-4-1.3l9.8-1.9v-4l-2-6.6c2.2 0.53339 4.2666 1.4 6.2 2.6l-1.2 5.5c-0.00004 0.40004 0.23329 0.73338 0.7 1l6.6-1.4c1.0666 0.0667 2.2666 0.50004 3.6 1.3-1.7334 0.93338-5.0334 2.0334-9.9 3.3-0.53337 0.13337-1 0.50004-1.4 1.1v3c0.0666 0.13337 0.29996 0.20004 0.7 0.2 0.26663 0.00004 1.9333-0.29996 5-0.9 3.1333-0.66663 4.8333-0.99996 5.1-1 0.53328 0.00004 1.6666 0.40004 3.4 1.2 1.8 0.73337 2.7 1.5334 2.7 2.4-0.00005 0.33337-0.23339 0.90003-0.7 1.7-0.46672 0.80003-1 1.6667-1.6 2.6-0.60005 0.8667-0.96672 1.4334-1.1 1.7l-3.5 8.3h-1.7m-8.3-10.5c0.79996 0.20003 1.4666 0.30003 2 0.3 0.33329 0.00003 0.63329-0.0333 0.9-0.1 0.33329-0.1333 0.53329-0.19997 0.6-0.2 2 0.00003 3.1333 0.4667 3.4 1.4-1 0.53336-3.3 1.2667-6.9 2.2v2.9c0.3333 0.40002 1.1 0.60002 2.3 0.6l6-0.6c1.1333-4.4666 1.7-7.1666 1.7-8.1-0.00005-1.2-0.16671-1.9333-0.5-2.2h-6.1c-0.46671 0.00003-1.6 0.30003-3.4 0.9v2.9m-2.7 1.3v-3.7h-1.4l-9 1.7 3.1 9.5 5.5-0.9c0.46663 0.13336 1.0333 0.36669 1.7 0.7v-4.1c-0.00004-0.19997-0.10004-0.29997-0.3-0.3-1.8 0.20003-2.8667 0.30003-3.2 0.3-0.0667 0.00003-0.20003 0.00003-0.4 0-0.13336 0.00003-0.20003 0.00003-0.2 0-0.73336 0.00003-1.2667-0.0666-1.6-0.2-0.33336-0.19997-0.73336-0.59997-1.2-1.2l7-1.8" fill="#fff"/> </g> </svg>
HTML Content
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mahjong Solitaire</title> <style> *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} :root{ --bg:#12100e;--bg-s:#1e1b17;--bg-e:#2a2620; --tf:#f0e4d0;--tf2:#ddd0b8;--tb:#b8956a;--ts:rgba(0,0,0,0.5); --ac:#d4a04a;--acd:rgba(212,160,74,0.2);--acg:rgba(212,160,74,0.5); --red:#c0392b;--grn:#2d8c4e;--blu:#1a6eb5; --tx:#e8dcc8;--txd:#8a7e6e;--txdk:#1a1510; --ov:rgba(0,0,0,0.75); --bb:#2a2620;--bbh:#3a352e;--bbt:#e8dcc8;--bbb:#4a4238; } .light{ --bg:#e8dfd2;--bg-s:#f2ebe0;--bg-e:#faf5ed; --tf:#fffdf7;--tf2:#f5eedd;--tb:#c4a882;--ts:rgba(0,0,0,0.12); --ac:#b8860b;--acd:rgba(184,134,11,0.15);--acg:rgba(184,134,11,0.4); --red:#a03020;--grn:#1a6b3a;--blu:#1565a0; --tx:#2c1810;--txd:#7a6a58;--txdk:#1a1510; --ov:rgba(0,0,0,0.5); --bb:#e0d5c5;--bbh:#d5c8b5;--bbt:#2c1810;--bbb:#c0b09a; } html{height:100%} body{ min-height:100%;font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg);color:var(--tx); transition:background .4s,color .4s;overflow-x:hidden; } .bg-blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.04;pointer-events:none;z-index:0} .blob-1{width:500px;height:500px;background:var(--ac);top:-150px;right:-100px;animation:fb1 22s ease-in-out infinite} .blob-2{width:400px;height:400px;background:var(--red);bottom:-100px;left:-100px;animation:fb2 28s ease-in-out infinite} .blob-3{width:300px;height:300px;background:var(--grn);top:50%;left:50%;transform:translate(-50%,-50%);animation:fb3 18s ease-in-out infinite} @keyframes fb1{0%,100%{transform:translate(0,0)}50%{transform:translate(-40px,40px)}} @keyframes fb2{0%,100%{transform:translate(0,0)}50%{transform:translate(30px,-30px)}} @keyframes fb3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.2)}} #app{display:flex;flex-direction:column;min-height:100vh;position:relative;z-index:1} header{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:var(--bg-s);border-bottom:1px solid var(--bbb);flex-wrap:wrap;gap:10px;transition:background .4s} .logo{font-size:22px;font-weight:800;letter-spacing:2px;color:var(--ac);text-transform:uppercase} .logo span{color:var(--tx);font-weight:300} .controls{display:flex;gap:6px;flex-wrap:wrap} .btn{background:var(--bb);color:var(--bbt);border:1px solid var(--bbb);padding:7px 14px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;transition:background .2s,transform .1s,box-shadow .2s;white-space:nowrap} .btn:hover{background:var(--bbh);box-shadow:0 2px 8px rgba(0,0,0,.2)} .btn:active{transform:scale(.96)} .btn:focus-visible{outline:2px solid var(--ac);outline-offset:2px} .btn svg{width:16px;height:16px;vertical-align:-3px;margin-right:4px} main{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:10px;position:relative} #board-wrap{position:relative;transition:transform .3s} #board{position:relative} .tile{ position:absolute;width:42px;height:56px;border-radius:5px; background:linear-gradient(145deg,var(--tf) 0%,var(--tf2) 100%); border:1px solid var(--tb);cursor:default; display:flex;align-items:center;justify-content:center; user-select:none;-webkit-user-select:none; transition:transform .15s ease,box-shadow .15s ease,opacity .3s; box-shadow:1px 2px 1px rgba(255,255,255,.15) inset,-1px -1px 1px rgba(0,0,0,.08) inset,var(--ls,2px 3px 5px) var(--ts); } .tile::before{content:'';position:absolute;inset:1px;border-radius:4px;border:1px solid rgba(255,255,255,.25);border-bottom-color:rgba(0,0,0,.08);border-right-color:rgba(0,0,0,.08);pointer-events:none} .tile svg{width:34px;height:48px;pointer-events:none} .tile.free{cursor:pointer} .tile.free:hover{transform:translateY(-2px);box-shadow:1px 2px 1px rgba(255,255,255,.15) inset,-1px -1px 1px rgba(0,0,0,.08) inset,var(--ls,2px 3px 5px) var(--ts),0 0 14px var(--acd)} .tile.selected{transform:translateY(-3px);border-color:var(--ac);box-shadow:1px 2px 1px rgba(255,255,255,.2) inset,-1px -1px 1px rgba(0,0,0,.08) inset,var(--ls,2px 3px 5px) var(--ts),0 0 18px var(--acg)} .tile.blocked{opacity:.65} .tile.hint{animation:hPulse .7s ease-in-out infinite} .tile.dealing{animation:dealIn .35s ease-out backwards} .tile.removing{animation:tileOut .4s ease-out forwards;pointer-events:none} @keyframes dealIn{from{transform:scale(.4) translateY(15px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}} @keyframes tileOut{0%{transform:scale(1);opacity:1}40%{transform:scale(1.12);opacity:.9}100%{transform:scale(0) rotate(15deg);opacity:0}} @keyframes hPulse{0%,100%{box-shadow:1px 2px 1px rgba(255,255,255,.15) inset,-1px -1px 1px rgba(0,0,0,.08) inset,var(--ls,2px 3px 5px) var(--ts),0 0 8px var(--acd)}50%{box-shadow:1px 2px 1px rgba(255,255,255,.15) inset,-1px -1px 1px rgba(0,0,0,.08) inset,var(--ls,2px 3px 5px) var(--ts),0 0 22px var(--acg)}} footer{display:flex;justify-content:center;gap:28px;padding:10px 20px;background:var(--bg-s);border-top:1px solid var(--bbb);flex-wrap:wrap;transition:background .4s} .stat{text-align:center;min-width:70px} .stat-label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--txd);margin-bottom:2px} .stat-value{display:block;font-size:20px;font-weight:700;color:var(--ac);font-variant-numeric:tabular-nums} .overlay{position:fixed;inset:0;background:var(--ov);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;pointer-events:none;transition:opacity .3s} .overlay.show{opacity:1;pointer-events:all} .ov-box{background:var(--bg-e);border:1px solid var(--bbb);border-radius:16px;padding:36px 44px;text-align:center;max-width:380px;width:90%;transform:scale(.9);transition:transform .3s} .overlay.show .ov-box{transform:scale(1)} .ov-box h2{font-size:28px;font-weight:800;color:var(--ac);margin-bottom:8px} .ov-box p{color:var(--txd);margin-bottom:6px;font-size:15px;line-height:1.5} .ov-box p strong{color:var(--tx)} .ov-box .btn-row{display:flex;gap:10px;justify-content:center;margin-top:18px} .ov-box .btn{padding:10px 24px;font-size:15px} #confetti{position:fixed;inset:0;pointer-events:none;z-index:999;overflow:hidden} .c-p{position:absolute;top:-12px;width:8px;height:12px;border-radius:2px;animation:cFall linear forwards} @keyframes cFall{0%{transform:translateY(0) rotate(0deg) scale(1);opacity:1}80%{opacity:1}100%{transform:translateY(105vh) rotate(900deg) scale(.5);opacity:0}} .toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--bg-e);border:1px solid var(--bbb);color:var(--tx);padding:10px 22px;border-radius:8px;font-size:14px;opacity:0;transition:all .3s;z-index:500;pointer-events:none;white-space:nowrap} .toast.show{opacity:1;transform:translateX(-50%) translateY(0)} @media(max-width:600px){ header{padding:10px 14px} .logo{font-size:18px} .btn{padding:6px 10px;font-size:12px} .btn svg{width:14px;height:14px} footer{gap:16px;padding:8px 14px} .stat-value{font-size:17px} .stat-label{font-size:9px} } @media(prefers-reduced-motion:reduce){ .tile,.tile.free,.tile.selected,.overlay,.ov-box,.toast{transition:none!important} .tile.dealing,.tile.removing,.tile.hint,.c-p,.bg-blob{animation:none!important} } </style> </head> <body> <div class="bg-blob blob-1"></div> <div class="bg-blob blob-2"></div> <div class="bg-blob blob-3"></div> <div id="app"> <header> <div class="logo">Mahjong</div> <div class="controls"> <button class="btn" id="btnNew" title="New Game (N)"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/></svg>New </button> <button class="btn" id="btnBlock" title="Blocked (B)"> <!-- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/></svg>Blocked --> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="2" fill="none"></path><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" fill="none"></circle></svg>Blocked </button> <button class="btn" id="btnHint" title="Hint (H)"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M6 8a2 2 0 1 1 2 2v1"/></svg>Hint </button> <button class="btn" id="btnShuffle" title="Shuffle (S)"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h8l2 3-2 3H2M2 12h8l2-3"/></svg>Shuffle </button> <button class="btn" id="btnUndo" title="Undo (Z)"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 7h7a3 3 0 1 1 0 6H8"/><path d="M6 4L3 7l3 3"/></svg>Undo </button> <button class="btn" id="btnTheme" title="Toggle Theme (T)"> <svg id="themeIcon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41"/></svg> </button> </div> </header> <main> <div id="board-wrap"><div id="board"></div></div> </main> <footer> <div class="stat"><span class="stat-label">Moves</span><span class="stat-value" id="sMoves">0</span></div> <div class="stat"><span class="stat-label">Time</span><span class="stat-value" id="sTime">0:00</span></div> <div class="stat"><span class="stat-label">Tiles</span><span class="stat-value" id="sTiles">144</span></div> <div class="stat"><span class="stat-label">Pairs</span><span class="stat-value" id="sPairs">0</span></div> </footer> </div> <div class="overlay" id="ovWin"> <div class="ov-box"> <h2>Victory</h2> <p>All tiles cleared in <strong id="wTime"></strong></p> <p><strong id="wMoves"></strong> moves</p> <div class="btn-row"><button class="btn" id="wBtn">Play Again</button></div> </div> </div> <div class="overlay" id="ovStuck"> <div class="ov-box"> <h2>No Moves Left</h2> <p>Shuffle the remaining tiles or start fresh.</p> <div class="btn-row"> <button class="btn" id="stShuffle">Shuffle</button> <button class="btn" id="stNew">New Game</button> </div> </div> </div> <div id="confetti"></div> <div class="toast" id="toast"></div> <script> /* ===== Constants ===== */ var TW=42,TH=56; var LDX=-1,LDY=-1; /* ===== Tile Types (36 types x 4 = 144) ===== */ var TYPES=[]; var i; for(i=1;i<=9;i++) TYPES.push({suit:'bam',val:i,lbl:'Bamboo '+i}); for(i=1;i<=9;i++) TYPES.push({suit:'cir',val:i,lbl:'Circles '+i}); for(i=1;i<=9;i++) TYPES.push({suit:'chr',val:i,lbl:'Character '+i}); ['E','S','W','N'].forEach(function(d,vi){TYPES.push({suit:'win',val:d,lbl:['East','South','West','North'][vi]+' Wind'})}); ['R','G','W'].forEach(function(d,vi){TYPES.push({suit:'dra',val:d,lbl:['Red','Green','White'][vi]+' Dragon'})}); /* Season tiles: all 4 match each other (index 34) */ TYPES.push({suit:'sea',val:'S',lbl:'Season'}); /* Flower tiles: all 4 match each other (index 35) */ TYPES.push({suit:'flw',val:'F',lbl:'Flower'}); /* ===== Layout (144 positions across 5 layers) ===== */ function rng(a,b){var r=[];for(var i=a;i<b;i++)r.push(i);return r} function L(cols,row,z,ox,oy){cols.forEach(function(c){LAYOUT.push([c+ox,row+oy,z])})} var LAYOUT=[]; L([3,4,7,8],0,0,0,0);L(rng(3,9),1,0,0,0);L(rng(2,11),2,0,0,0);L(rng(1,11),3,0,0,0); L([0,1,2,3,7,8,9,10],4,0,0,0);L([0,1,2,3,7,8,9,10],5,0,0,0); L(rng(2,10),6,0,0,0);L(rng(2,9),7,0,0,0);L([3,4,7,8],8,0,0,0); L([2,3,6,7],0,1,.5,.5);L(rng(2,8),1,1,.5,.5);L(rng(1,8),2,1,.5,.5); L([0,1,2,6,7,8],3,1,.5,.5);L([0,1,2,6,7,8],4,1,.5,.5); L(rng(1,8),5,1,.5,.5);L([2,3,6,7],6,1,.5,.5); L([2,3,5,6],0,2,0,1);L(rng(0,8),1,2,0,1);L(rng(0,8),2,2,0,1);L([2,3,5,6],3,2,0,1); L(rng(1,7),0,3,.5,1.5);L(rng(1,7),1,3,.5,1.5); L(rng(2,6),0,4,0,2); /* ===== SVG Generation ===== */ var FF="'Noto Serif CJK SC','SimSun','STSong','Songti SC','MS Mincho',serif"; function svgWrap(inner){return '<svg viewBox="0 0 42 56" xmlns="http://www.w3.org/2000/svg">'+inner+'</svg>'} function svgBamboo(n){ var pos=bamPos(n),s=''; pos.forEach(function(p){ var px=p[0],py=p[1],w=5,h=16,r=2.2; s+='<rect x="'+(px-w/2)+'" y="'+(py-h/2)+'" width="'+w+'" height="'+h+'" rx="'+r+'" fill="#2a8548" stroke="#1a5c32" stroke-width=".6"/>'; s+='<line x1="'+(px-w/2)+'" y1="'+(py-h/4)+'" x2="'+(px+w/2)+'" y2="'+(py-h/4)+'" stroke="#1a5c32" stroke-width=".7"/>'; s+='<line x1="'+(px-w/2)+'" y1="'+(py+h/5)+'" x2="'+(px+w/2)+'" y2="'+(py+h/5)+'" stroke="#1a5c32" stroke-width=".7"/>'; }); return svgWrap(s); } function bamPos(n){ var c=21,y=28; switch(n){ case 1:return[[c,y]]; case 2:return[[c-5,y],[c+5,y]]; case 3:return[[c-6,y],[c,y],[c+6,y]]; case 4:return[[c-5,y-9],[c+5,y-9],[c-5,y+9],[c+5,y+9]]; case 5:return[[c-6,y-18],[c+6,y-18],[c,y],[c-6,y+18],[c+6,y+18]]; case 6:return[[c-5,y-18],[c+5,y-18],[c-5,y],[c+5,y],[c-5,y+18],[c+5,y+18]]; case 7:return[[c-7,y-18],[c,y-18],[c+7,y-18],[c,y],[c-7,y+18],[c,y+18],[c+7,y+18]]; case 8:return[[c-7,y-18],[c,y-18],[c+7,y-18],[c-4,y],[c+4,y],[c-7,y+18],[c,y+18],[c+7,y+18]]; case 9:return[[c-7,y-18],[c,y-18],[c+7,y-18],[c-7,y],[c,y],[c+7,y],[c-7,y+18],[c,y+18],[c+7,y+18]]; default:return[[c,y]]; } } function svgCircles(n){ var pos=cirPos(n),s=''; pos.forEach(function(p){ var cx=p[0],cy=p[1],r=p[2]; s+='<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="#1868a8" stroke="#0e4a78" stroke-width=".5"/>'; s+='<circle cx="'+(cx-.8)+'" cy="'+(cy-.8)+'" r="'+(r*.4)+'" fill="rgba(255,255,255,.18)"/>'; }); return svgWrap(s); } function cirPos(n){ var c=21,y=28; switch(n){ case 1:return[[c,y,8.5]]; case 2:return[[c,y-9,4.8],[c,y+9,4.8]]; case 3:return[[c-5,y-9,4.2],[c+5,y,4.2],[c-5,y+9,4.2]]; case 4:return[[c-5,y-5.5,4.2],[c+5,y-5.5,4.2],[c-5,y+5.5,4.2],[c+5,y+5.5,4.2]]; case 5:return[[c-6,y-7,3.6],[c+6,y-7,3.6],[c,y,3.6],[c-6,y+7,3.6],[c+6,y+7,3.6]]; case 6:return[[c-5,y-7,3.5],[c+5,y-7,3.5],[c-5,y,3.5],[c+5,y,3.5],[c-5,y+7,3.5],[c+5,y+7,3.5]]; case 7:return[[c-7,y-8,3],[c,y-8,3],[c+7,y-8,3],[c,y,3],[c-7,y+8,3],[c,y+8,3],[c+7,y+8,3]]; case 8:return[[c-7,y-8,3],[c,y-8,3],[c+7,y-8,3],[c-4,y,3],[c+4,y,3],[c-7,y+8,3],[c,y+8,3],[c+7,y+8,3]]; case 9:return[[c-7,y-8,3],[c,y-8,3],[c+7,y-8,3],[c-7,y,3],[c,y,3],[c+7,y,3],[c-7,y+8,3],[c,y+8,3],[c+7,y+8,3]]; default:return[[c,y,8]]; } } function svgChar(n){ var ch=['一','二','三','四','五','六','七','八','九'][n-1]; return svgWrap('<text x="21" y="26" text-anchor="middle" dy=".38em" font-size="28" fill="#b83225" font-weight="bold" font-family="'+FF+'">'+ch+'</text><text x="34" y="50" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.22)" font-family="sans-serif">'+n+'</text>'); } function svgWind(d){ var ch={E:'東',S:'南',W:'西',N:'北'}[d]; var sub={E:'E',S:'S',W:'W',N:'N'}[d]; return svgWrap('<text x="21" y="26" text-anchor="middle" dy=".38em" font-size="24" fill="#1a1510" font-weight="bold" font-family="'+FF+'">'+ch+'</text><text x="34" y="50" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.22)" font-family="sans-serif">'+sub+'</text>'); } function svgDragon(t){ if(t==='R') return svgWrap('<text x="21" y="26" text-anchor="middle" dy=".38em" font-size="26" fill="#b83225" font-weight="bold" font-family="'+FF+'">中</text><text x="34" y="50" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.22)" font-family="sans-serif">R</text>'); if(t==='G') return svgWrap('<text x="21" y="26" text-anchor="middle" dy=".38em" font-size="22" fill="#1f7a3f" font-weight="bold" font-family="'+FF+'">發</text><text x="34" y="50" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.22)" font-family="sans-serif">G</text>'); return svgWrap('<rect x="11" y="13" width="20" height="26" rx="2.5" fill="none" stroke="#1a1510" stroke-width="2.2"/><rect x="14" y="16" width="14" height="20" rx="1.5" fill="none" stroke="#1a1510" stroke-width=".8" opacity=".35"/><text x="34" y="50" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.22)" font-family="sans-serif">W</text>'); } function svgSeason(){ /* Teal leaf + character */ return svgWrap( '<path d="M21 10 C17 14 13 20 13 25 C13 30 16 32 21 32 C26 32 29 30 29 25 C29 20 25 14 21 10Z" fill="#1a8a7a" stroke="#13665a" stroke-width=".6" opacity=".9"/>'+ '<line x1="21" y1="10" x2="21" y2="32" stroke="#13665a" stroke-width=".5" opacity=".5"/>'+ '<text x="21" y="48" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.25)" font-family="sans-serif">SEASON</text>' ); } function svgFlower(){ /* Pink flower with center */ var s=''; var petals=[[21,16],[16,21],[16,27],[21,32],[26,27],[26,21]]; petals.forEach(function(p){ s+='<circle cx="'+p[0]+'" cy="'+p[1]+'" r="4.2" fill="#c8506a" stroke="#a03850" stroke-width=".4" opacity=".75"/>'; }); s+='<circle cx="21" cy="24" r="4" fill="#f0c840" stroke="#d4a820" stroke-width=".4"/>'; s+='<circle cx="21" cy="48" r="0" fill="none"/>'; s+='<text x="21" y="48" text-anchor="middle" font-size="7" fill="rgba(0,0,0,.25)" font-family="sans-serif">FLOWER</text>'; return svgWrap(s); } function tileSVG(type){ if(type.suit==='bam') return svgBamboo(type.val); if(type.suit==='cir') return svgCircles(type.val); if(type.suit==='chr') return svgChar(type.val); if(type.suit==='win') return svgWind(type.val); if(type.suit==='dra') return svgDragon(type.val); if(type.suit==='sea') return svgSeason(); if(type.suit==='flw') return svgFlower(); return svgWrap(''); } /* Cache SVGs */ var SVG_CACHE=TYPES.map(function(t){return tileSVG(t)}); /* ===== Game State ===== */ var S={ tiles:[], sel:null, moves:0, pairs:0, timer:0, tInt:null, tStarted:false, remaining:144, undo:[], hints:[], over:false, blocked:true }; /* ===== Board Dimensions ===== */ var BX0=Infinity,BY0=Infinity,BX1=-Infinity,BY1=-Infinity; LAYOUT.forEach(function(p){var x=p[0],y=p[1];BX0=Math.min(BX0,x);BY0=Math.min(BY0,y);BX1=Math.max(BX1,x+1);BY1=Math.max(BY1,y+1)}); var BW=(BX1-BX0)*TW, BH=(BY1-BY0)*TH; /* ===== Free Tile Check ===== */ function isFree(t){ if(t.removed) return false; var e=.01; /* Blocked from above? */ var above=false; for(var i=0;i<S.tiles.length;i++){ var o=S.tiles[i]; if(!o.removed&&o.z===t.z+1&&Math.abs(o.x-t.x)<1&&Math.abs(o.y-t.y)<1){above=true;break} } if(above) return false; /* Left or right free? */ var bl=false,br=false; for(var i=0;i<S.tiles.length;i++){ var o=S.tiles[i]; if(!o.removed&&o.z===t.z&&Math.abs(o.y-t.y)<e){ if(Math.abs(o.x-(t.x-1))<e) bl=true; if(Math.abs(o.x-(t.x+1))<e) br=true; } } return !bl||!br; } /* ===== Find Available Moves ===== */ function findMoves(){ var free=[]; for(var i=0;i<S.tiles.length;i++){ if(!S.tiles[i].removed&&isFree(S.tiles[i])) free.push(S.tiles[i]); } var moves=[]; for(var i=0;i<free.length;i++){ for(var j=i+1;j<free.length;j++){ if(free[i].ti===free[j].ti) moves.push([free[i],free[j]]); } } return moves; } /* ===== Shuffle ===== */ function shuffle(arr){ for(var i=arr.length-1;i>0;i--){var j=Math.floor(Math.random()*(i+1));var tmp=arr[i];arr[i]=arr[j];arr[j]=tmp} return arr; } /* ===== Initialize Game ===== */ function initGame(){ clearInterval(S.tInt); var typeIndices=[]; for(var i=0;i<36;i++) for(var j=0;j<4;j++) typeIndices.push(i); shuffle(typeIndices); S.tiles=LAYOUT.map(function(p,idx){return {id:idx,x:p[0],y:p[1],z:p[2],ti:typeIndices[idx],removed:false,el:null}}); S.sel=null;S.moves=0;S.pairs=0;S.timer=0;S.tStarted=false;S.remaining=144;S.undo=[];S.hints=[];S.over=false; renderBoard(); updateStats(); hideOv('ovWin'); hideOv('ovStuck'); document.getElementById('confetti').innerHTML=''; } /* ===== Render Board ===== */ function renderBoard(){ var board=document.getElementById('board'); board.innerHTML=''; board.style.width=BW+'px'; board.style.height=BH+'px'; S.tiles.forEach(function(t){ var el=document.createElement('div'); el.className='tile dealing'; el.innerHTML=SVG_CACHE[t.ti]; el.title=TYPES[t.ti].lbl; el.setAttribute('role','button'); el.setAttribute('aria-label',TYPES[t.ti].lbl); el.setAttribute('tabindex','-1'); var px=(t.x-BX0)*TW+t.z*LDX; var py=(t.y-BY0)*TH+t.z*LDY; el.style.left=px+'px'; el.style.top=py+'px'; el.style.zIndex=t.z*100+t.y*2+Math.round(t.x*10); el.style.setProperty('--ls',(2+t.z*2)+'px '+(3+t.z*2)+'px '+(5+t.z*3)+'px'); el.style.animationDelay=(t.z*100+t.y*10+t.x*4)+'ms'; el.addEventListener('click',(function(tile){return function(){onClick(tile)}})(t)); board.appendChild(el); t.el=el; }); setTimeout(function(){ S.tiles.forEach(function(t){t.el.classList.remove('dealing');updateTileState(t, S.blocked)}); },950); scaleBoard(); } /* ===== Update Single Tile State ===== */ function updateTileState(t, hideBlocked=false){ if(t.removed||!t.el) return; var f=isFree(t); t.el.classList.toggle('free',f); if(hideBlocked){ t.el.classList.remove('blocked'); } else { t.el.classList.toggle('blocked',!f); } t.el.classList.remove('selected','hint'); } /* ===== Update All Tile States ===== */ function updateAllStates(){ clearHints(); S.tiles.forEach(function(t){updateTileState(t, S.blocked)}); if(S.sel&&!S.sel.removed&&S.sel.el){ if(isFree(S.sel)) S.sel.el.classList.add('selected'); else S.sel=null; } } /* ===== Scale Board ===== */ function scaleBoard(){ var wrap=document.getElementById('board-wrap'); var main=wrap.parentElement; var mw=main.clientWidth-20, mh=main.clientHeight-20; var sx=mw/(BW+20), sy=mh/(BH+20); var sc=Math.min(1,sx,sy); wrap.style.transform='scale('+sc+')'; wrap.style.transformOrigin='top center'; } window.addEventListener('resize',scaleBoard); /* ===== Click Handler ===== */ function onClick(t){ if(S.over||t.removed) return; clearHints(); if(!isFree(t)) return; if(!S.tStarted){S.tStarted=true;startTimer()} if(!S.sel){S.sel=t;t.el.classList.add('selected');return} if(S.sel.id===t.id){S.sel.el.classList.remove('selected');S.sel=null;return} if(S.sel.ti===t.ti){removePair(S.sel,t);return} S.sel.el.classList.remove('selected');S.sel=t;t.el.classList.add('selected'); } /* ===== Remove Pair ===== */ function removePair(a,b){ clearHints(); S.undo.push({a:{id:a.id,ti:a.ti},b:{id:b.id,ti:b.ti}}); a.removed=true;b.removed=true; a.el.classList.remove('selected');b.el.classList.remove('selected','free','blocked'); a.el.classList.add('removing');b.el.classList.add('removing'); S.sel=null;S.moves++;S.pairs++;S.remaining-=2; updateStats(); setTimeout(function(){ if(a.el.parentNode) a.el.remove(); if(b.el.parentNode) b.el.remove(); updateAllStates(); if(S.remaining===0) winGame(); else if(findMoves().length===0&&S.remaining>0) showStuck(); },420); } /* ===== Undo ===== */ function undo(){ if(!S.undo.length||S.over) return; var m=S.undo.pop(); var a=S.tiles[m.a.id], b=S.tiles[m.b.id]; a.removed=false;b.removed=false;a.ti=m.a.ti;b.ti=m.b.ti; [a,b].forEach(function(t){ var el=document.createElement('div'); el.className='tile'; el.innerHTML=SVG_CACHE[t.ti]; el.title=TYPES[t.ti].lbl; el.setAttribute('role','button'); el.setAttribute('aria-label',TYPES[t.ti].lbl); el.setAttribute('tabindex','-1'); var px=(t.x-BX0)*TW+t.z*LDX; var py=(t.y-BY0)*TH+t.z*LDY; el.style.left=px+'px';el.style.top=py+'px'; el.style.zIndex=t.z*100+t.y*2+Math.round(t.x*10); el.style.setProperty('--ls',(2+t.z*2)+'px '+(3+t.z*2)+'px '+(5+t.z*3)+'px'); el.addEventListener('click',(function(tile){return function(){onClick(tile)}})(t)); document.getElementById('board').appendChild(el); t.el=el; }); S.moves=Math.max(0,S.moves-1);S.pairs=Math.max(0,S.pairs-1);S.remaining+=2;S.sel=null; updateStats();updateAllStates();toast('Move undone'); } /* ===== Shuffle Remaining ===== */ function doShuffle(attempt){ if(typeof attempt==='undefined') attempt=0; if(S.over) return; var rem=S.tiles.filter(function(t){return !t.removed}); if(!rem.length) return; var types=shuffle(rem.map(function(t){return t.ti})); rem.forEach(function(t,i){t.ti=types[i];t.el.innerHTML=SVG_CACHE[t.ti];t.el.title=TYPES[t.ti].lbl;t.el.setAttribute('aria-label',TYPES[t.ti].lbl)}); S.sel=null;S.undo=[]; updateAllStates(); if(findMoves().length===0){ if(attempt<200) doShuffle(attempt+1); else showStuck(); } else { toast('Tiles shuffled'); } } /* ===== Blocked ===== */ function doBlock(){ setTimeout(function(){ S.blocked = !S.blocked S.tiles.forEach(function(t){updateTileState(t, S.blocked)}); },500); } /* ===== Hint ===== */ function doHint(){ if(S.over) return; clearHints(); var moves=findMoves(); if(!moves.length){showStuck();return} var pair=moves[Math.floor(Math.random()*moves.length)]; S.hints=[pair[0],pair[1]]; pair[0].el.classList.add('hint');pair[1].el.classList.add('hint'); setTimeout(clearHints,2500); } function clearHints(){S.hints.forEach(function(t){if(t.el)t.el.classList.remove('hint')});S.hints=[]} /* ===== Timer ===== */ function startTimer(){ clearInterval(S.tInt); S.tInt=setInterval(function(){S.timer++;document.getElementById('sTime').innerHTML=fmtTime(S.timer)},1000); } function fmtTime(s){var m=Math.floor(s/60);return m+':'+(s%60<10?'0':'')+s%60} /* ===== Stats ===== */ function updateStats(){ document.getElementById('sMoves').textContent=S.moves; document.getElementById('sTiles').textContent=S.remaining; document.getElementById('sPairs').textContent=S.pairs; if(!S.tStarted) document.getElementById('sTime').innerHTML='0:00'; } /* ===== Win ===== */ function winGame(){ S.over=true;clearInterval(S.tInt); document.getElementById('wTime').textContent=fmtTime(S.timer); document.getElementById('wMoves').textContent=S.moves; setTimeout(function(){showOv('ovWin');spawnConfetti()},500); } function spawnConfetti(){ var c=document.getElementById('confetti');c.innerHTML=''; var cols=['#d4a04a','#c0392b','#2d8c4e','#1a6eb5','#f0e4d0','#e67e22','#c8506a','#1a8a7a']; for(var i=0;i<80;i++){ var p=document.createElement('div');p.className='c-p'; p.style.left=Math.random()*100+'%'; p.style.background=cols[Math.floor(Math.random()*cols.length)]; p.style.animationDelay=Math.random()*2.5+'s'; p.style.animationDuration=(2.5+Math.random()*3)+'s'; p.style.width=(6+Math.random()*6)+'px'; p.style.height=(8+Math.random()*10)+'px'; p.style.borderRadius=Math.random()>.5?'50%':'2px'; c.appendChild(p); } setTimeout(function(){c.innerHTML=''},7000); } /* ===== Stuck ===== */ function showStuck(){S.over=true;showOv('ovStuck')} /* ===== Overlays ===== */ function showOv(id){document.getElementById(id).classList.add('show')} function hideOv(id){document.getElementById(id).classList.remove('show')} /* ===== Toast ===== */ function toast(msg){ var t=document.getElementById('toast');t.textContent=msg;t.classList.add('show'); clearTimeout(t._t);t._t=setTimeout(function(){t.classList.remove('show')},1800); } /* ===== Theme ===== */ var dark=true; function toggleTheme(){ dark=!dark; document.body.classList.toggle('light',!dark); var icon=document.getElementById('themeIcon'); icon.innerHTML=dark ?'<circle cx="8" cy="8" r="3.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41"/>' :'<path d="M13 8A5 5 0 1 1 8 3a4 4 0 0 0 5 5z"/>'; } /* ===== Button Events ===== */ document.getElementById('btnNew').addEventListener('click',initGame); document.getElementById('btnBlock').addEventListener('click',doBlock); document.getElementById('btnHint').addEventListener('click',doHint); document.getElementById('btnShuffle').addEventListener('click',function(){S.over=false;hideOv('ovStuck');doShuffle()}); document.getElementById('btnUndo').addEventListener('click',undo); document.getElementById('btnTheme').addEventListener('click',toggleTheme); document.getElementById('wBtn').addEventListener('click',initGame); document.getElementById('stShuffle').addEventListener('click',function(){hideOv('ovStuck');S.over=false;doShuffle()}); document.getElementById('stNew').addEventListener('click',initGame); /* ===== Keyboard Shortcuts ===== */ document.addEventListener('keydown',function(e){ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return; var k=e.key.toLowerCase(); if(k==='n') initGame(); else if(k==='b') doBlock(); else if(k==='h') doHint(); else if(k==='s'){S.over=false;hideOv('ovStuck');doShuffle()} else if(k==='z') undo(); else if(k==='t') toggleTheme(); }); /* ===== Start ===== */ initGame(); </script> </body> </html>
Cancel