Chotto.app
Login
Sign Up
ES
App Name
kuyawa / sudoku
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"> <!-- Outer border of the Sudoku grid (main 3x3 section) --> <rect x="5" y="5" width="90" height="90" rx="5" ry="5" fill="white" stroke="#000000" stroke-width="6"/> <!-- Thick grid lines dividing the 3x3 major blocks --> <line x1="35" y1="5" x2="35" y2="95" stroke="#000000" stroke-width="6"/> <line x1="65" y1="5" x2="65" y2="95" stroke="#000000" stroke-width="6"/> <line x1="5" y1="35" x2="95" y2="35" stroke="#000000" stroke-width="6"/> <line x1="5" y1="65" x2="95" y2="65" stroke="#000000" stroke-width="6"/> <!-- Thin grid lines for the individual cells --> <!-- Vertical lines --> <line x1="15" y1="5" x2="15" y2="95" stroke="#000000" stroke-width="2"/> <line x1="25" y1="5" x2="25" y2="95" stroke="#000000" stroke-width="2"/> <line x1="45" y1="5" x2="45" y2="95" stroke="#000000" stroke-width="2"/> <line x1="55" y1="5" x2="55" y2="95" stroke="#000000" stroke-width="2"/> <line x1="75" y1="5" x2="75" y2="95" stroke="#000000" stroke-width="2"/> <line x1="85" y1="5" x2="85" y2="95" stroke="#000000" stroke-width="2"/> <!-- Horizontal lines --> <line x1="5" y1="15" x2="95" y2="15" stroke="#000000" stroke-width="2"/> <line x1="5" y1="25" x2="95" y2="25" stroke="#000000" stroke-width="2"/> <line x1="5" y1="45" x2="95" y2="45" stroke="#000000" stroke-width="2"/> <line x1="5" y1="55" x2="95" y2="55" stroke="#000000" stroke-width="2"/> <line x1="5" y1="75" x2="95" y2="75" stroke="#000000" stroke-width="2"/> <line x1="5" y1="85" x2="95" y2="85" stroke="#000000" stroke-width="2"/> <!-- Example numbers pre-filled in the icon --> <text x="20" y="22" font-family="Arial, sans-serif" font-size="14" fill="#000000" text-anchor="middle">5</text> <text x="50" y="52" font-family="Arial, sans-serif" font-size="14" fill="#000000" text-anchor="middle">3</text> <text x="80" y="82" font-family="Arial, sans-serif" font-size="14" fill="#000000" text-anchor="middle">9</text> </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.0"> <title>Sudoku</title> <style> :root { --speed: 0.3s; } [data-theme="dark"] { --bg-primary: #0e0e0e; --bg-secondary: #181818; --bg-tertiary: #222; --bg-cell: #1a1a1a; --bg-cell-hover: #272727; --bg-cell-selected: #3d2c0a; --bg-cell-highlight: #261f10; --bg-cell-same: #2e2410; --bg-cell-error: #3a1212; --bg-cell-given: #161616; --border-thin: #2a2a2a; --border-thick: #444; --text-primary: #e4e4e4; --text-secondary: #777; --text-given: #f2f2f2; --text-user: #e8a830; --text-error: #e84848; --text-note: #666; --accent: #e8a830; --accent-hover: #f2b840; --accent-glow: rgba(232,168,48,0.12); --shadow: rgba(0,0,0,0.5); --success: #48c868; --overlay: rgba(0,0,0,0.75); } [data-theme="light"] { --bg-primary: #f2ede4; --bg-secondary: #e8e2d6; --bg-tertiary: #ddd6c8; --bg-cell: #faf8f4; --bg-cell-hover: #f0ece4; --bg-cell-selected: #fff4d4; --bg-cell-highlight: #f6f0e2; --bg-cell-same: #f4ecce; --bg-cell-error: #fce4e4; --bg-cell-given: #eeeadf; --border-thin: #ccc4b4; --border-thick: #9a9080; --text-primary: #2a2418; --text-secondary: #8a7e6e; --text-given: #1a1408; --text-user: #b87818; --text-error: #c42828; --text-note: #a09484; --accent: #b87818; --accent-hover: #c88820; --accent-glow: rgba(184,120,24,0.1); --shadow: rgba(0,0,0,0.08); --success: #28884a; --overlay: rgba(0,0,0,0.45); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; display: flex; flex-direction: column; align-items: center; transition: background var(--speed), color var(--speed); overflow-x: hidden; } [data-theme="dark"] body::before { content: ''; position: fixed; inset: 0; background: radial-gradient(ellipse at 15% 5%, rgba(232,168,48,0.04) 0%, transparent 55%), radial-gradient(ellipse at 85% 95%, rgba(232,168,48,0.025) 0%, transparent 55%); pointer-events: none; z-index: 0; } body > * { position: relative; z-index: 1; } .header { width: 100%; max-width: 520px; padding: 20px 16px 0; display: flex; justify-content: space-between; align-items: center; } .logo { font-size: 22px; font-weight: 900; letter-spacing: 5px; text-transform: uppercase; } .logo span { color: var(--accent); } .icon-btn { width: 42px; height: 42px; border: 1px solid var(--border-thin); background: var(--bg-secondary); color: var(--text-secondary); border-radius: 11px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .icon-btn:hover { background: var(--bg-tertiary); color: var(--accent); border-color: var(--accent); } .icon-btn svg { width: 18px; height: 18px; } .bar { width: 100%; max-width: 520px; padding: 14px 16px 6px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } .diff-group { display: flex; gap: 3px; background: var(--bg-secondary); border-radius: 9px; padding: 3px; border: 1px solid var(--border-thin); } .diff-btn { padding: 5px 13px; border: none; background: transparent; color: var(--text-secondary); font-size: 12.5px; font-weight: 700; border-radius: 7px; cursor: pointer; transition: all 0.2s; } .diff-btn.active { background: var(--accent); color: #000; } .diff-btn:not(.active):hover { color: var(--text-primary); background: var(--bg-tertiary); } .info { display: flex; gap: 16px; align-items: center; } .timer, .mistakes-label { font-size: 13.5px; color: var(--text-secondary); font-weight: 700; font-variant-numeric: tabular-nums; } .mistakes-label .err { color: var(--text-error); } /* Grid */ .grid-wrap { width: 100%; max-width: 520px; padding: 8px 16px; } .grid { display: grid; grid-template-columns: repeat(9,1fr); background: var(--border-thick); border: 2.5px solid var(--border-thick); border-radius: 12px; overflow: hidden; box-shadow: 0 8px 36px var(--shadow); aspect-ratio: 1; } .cell { background: var(--bg-cell); display: flex; align-items: center; justify-content: center; font-size: clamp(17px,4.2vw,26px); font-weight: 700; cursor: pointer; position: relative; transition: background 0.12s; border-right: 1px solid var(--border-thin); border-bottom: 1px solid var(--border-thin); user-select: none; -webkit-user-select: none; } .cell:nth-child(3n) { border-right: 2.5px solid var(--border-thick); } .cell:nth-child(9n) { border-right: none; } .cell:nth-child(n+19):nth-child(-n+27), .cell:nth-child(n+46):nth-child(-n+54) { border-bottom: 2.5px solid var(--border-thick); } .cell:nth-child(n+73) { border-bottom: none; } .cell.given { color: var(--text-given); font-weight: 800; } .cell.user { color: var(--text-user); } .cell.selected { background: var(--bg-cell-selected); z-index: 1; } .cell.hl { background: var(--bg-cell-highlight); } .cell.same { background: var(--bg-cell-same); } .cell.error { background: var(--bg-cell-error); color: var(--text-error); animation: shake 0.3s ease; } .cell:hover:not(.selected) { background: var(--bg-cell-hover); } .notes-g { display: grid; grid-template-columns: repeat(3,1fr); grid-template-rows: repeat(3,1fr); position: absolute; inset: 0; } .notes-g span { display: flex; align-items: center; justify-content: center; font-size: clamp(7.5px,1.8vw,10.5px); font-weight: 600; color: var(--text-note); line-height: 1; } /* Numpad */ .pad-wrap { width: 100%; max-width: 520px; padding: 10px 16px; } .pad { display: grid; grid-template-columns: repeat(9,1fr); gap: 5px; } .num { aspect-ratio: 1; border: 1px solid var(--border-thin); background: var(--bg-secondary); color: var(--text-primary); font-size: clamp(15px,3.8vw,21px); font-weight: 700; border-radius: 9px; cursor: pointer; transition: all 0.15s; position: relative; } .num:hover { background: var(--bg-tertiary); border-color: var(--accent); color: var(--accent); transform: translateY(-1px); } .num:active { transform: translateY(0); } .num.done { opacity: 0.25; pointer-events: none; } .num .cnt { position: absolute; top: 2px; right: 4px; font-size: 8.5px; font-weight: 700; color: var(--text-secondary); } /* Actions */ .acts { width: 100%; max-width: 520px; padding: 4px 16px 16px; display: flex; gap: 6px; } .act { flex: 1; padding: 11px 4px; border: 1px solid var(--border-thin); background: var(--bg-secondary); color: var(--text-secondary); font-size: 12.5px; font-weight: 700; border-radius: 9px; cursor: pointer; transition: all 0.15s; display: flex; align-items: center; justify-content: center; gap: 5px; } .act:hover { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--accent); } .act.on { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); } .act svg { width: 15px; height: 15px; flex-shrink: 0; } .new-wrap { width: 100%; max-width: 520px; padding: 0 16px 28px; } .new-wrap button { width: 100%; padding: 13px; border: none; background: var(--accent); color: #000; font-size: 14px; font-weight: 800; border-radius: 11px; cursor: pointer; transition: all 0.2s; letter-spacing: 0.5px; } .new-wrap button:hover { background: var(--accent-hover); transform: translateY(-1px); box-shadow: 0 6px 24px var(--accent-glow); } /* Win overlay */ .overlay { position: fixed; inset: 0; background: var(--overlay); display: none; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .overlay.show { display: flex; animation: fadeIn 0.35s ease; } .card { background: var(--bg-secondary); border: 1px solid var(--border-thin); border-radius: 18px; padding: 36px 32px; text-align: center; max-width: 330px; width: 90%; box-shadow: 0 24px 64px rgba(0,0,0,0.5); animation: pop 0.35s ease; } .card h2 { font-size: 26px; font-weight: 900; color: var(--success); margin-bottom: 4px; } .card p { color: var(--text-secondary); font-size: 14px; } .stats { margin: 20px 0; display: flex; justify-content: center; gap: 28px; } .stat-v { font-size: 22px; font-weight: 800; color: var(--text-primary); } .stat-l { font-size: 11px; color: var(--text-secondary); margin-top: 2px; } .card button { margin-top: 12px; padding: 11px 30px; border: none; background: var(--accent); color: #000; font-size: 13px; font-weight: 800; border-radius: 9px; cursor: pointer; transition: all 0.2s; } .card button:hover { background: var(--accent-hover); } /* Toast */ .toast { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%) translateY(80px); background: var(--bg-tertiary); border: 1px solid var(--border-thin); color: var(--text-primary); padding: 10px 22px; border-radius: 10px; font-size: 13.5px; font-weight: 700; z-index: 200; transition: transform 0.3s ease; box-shadow: 0 8px 28px var(--shadow); } .toast.show { transform: translateX(-50%) translateY(0); } /* Animations */ @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-3px)} 75%{transform:translateX(3px)} } @keyframes fadeIn { from{opacity:0} to{opacity:1} } @keyframes pop { from{transform:scale(0.88);opacity:0} to{transform:scale(1);opacity:1} } @keyframes cellPop { 0%{transform:scale(0.4);opacity:0} 60%{transform:scale(1.12)} 100%{transform:scale(1);opacity:1} } .cell.pop { animation: cellPop 0.22s ease; } @media(max-width:400px){ .header{padding:14px 12px 0} .bar{padding:10px 12px 4px} .grid-wrap{padding:6px 12px} .pad-wrap{padding:8px 12px} .acts{padding:2px 12px 14px;gap:4px} .new-wrap{padding:0 12px 22px} .diff-btn{padding:4px 9px;font-size:11.5px} .act{padding:9px 2px;font-size:11.5px} } @media(prefers-reduced-motion:reduce){ *,*::before,*::after{animation-duration:0.01ms!important;transition-duration:0.01ms!important} } </style> </head> <body> <header class="header"> <div class="logo">SU<span>DO</span>KU</div> <button class="icon-btn" id="themeBtn" aria-label="Toggle theme"> <svg id="icoSun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/> <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/> </svg> <svg id="icoMoon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/> </svg> </button> </header> <div class="bar"> <div class="diff-group"> <button class="diff-btn" data-d="easy">Easy</button> <button class="diff-btn active" data-d="medium">Medium</button> <button class="diff-btn" data-d="hard">Hard</button> <button class="diff-btn" data-d="expert">Expert</button> </div> <div class="info"> <div class="timer" id="timer">00:00</div> <div class="mistakes-label">Mistakes: <span class="err" id="errCnt">0</span>/3</div> </div> </div> <div class="grid-wrap"><div class="grid" id="grid" role="grid" aria-label="Sudoku grid"></div></div> <div class="pad-wrap"><div class="pad" id="pad"></div></div> <div class="acts"> <button class="act" id="undoBtn" aria-label="Undo"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> Undo </button> <button class="act" id="eraseBtn" aria-label="Erase"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20H7L3 16l9-9 8 8-4 4z"/><line x1="18" y1="13" x2="11" y2="6"/></svg> Erase </button> <button class="act" id="notesBtn" aria-label="Toggle notes"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Notes </button> <button class="act" id="hintBtn" aria-label="Hint"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg> Hint </button> </div> <div class="new-wrap"><button id="newBtn">New Game</button></div> <div class="overlay" id="winOvl"> <div class="card"> <h2>Puzzle Complete</h2> <p>Well done, you solved it!</p> <div class="stats"> <div><div class="stat-v" id="wTime">00:00</div><div class="stat-l">Time</div></div> <div><div class="stat-v" id="wDiff">Medium</div><div class="stat-l">Difficulty</div></div> <div><div class="stat-v" id="wErr">0</div><div class="stat-l">Mistakes</div></div> </div> <button id="wNew">Play Again</button> </div> </div> <div class="toast" id="toast"></div> <script> /* ============ State ============ */ let board, sol, fixed, notes, sel, noteMode, errs, hist, over, diff, secs, tick; /* ============ Sudoku Engine ============ */ function valid(g, r, c, n) { for (let i = 0; i < 9; i++) { if (g[r*9+i] === n || g[i*9+c] === n) return false; } const br = (r/3|0)*3, bc = (c/3|0)*3; for (let dr = 0; dr < 3; dr++) for (let dc = 0; dc < 3; dc++) if (g[(br+dr)*9+bc+dc] === n) return false; return true; } function solve(g, rand) { for (let i = 0; i < 81; i++) { if (g[i] === 0) { const r = i/9|0, c = i%9; let nums = [1,2,3,4,5,6,7,8,9]; if (rand) for (let j = 8; j > 0; j--) { const k = Math.random()*(j+1)|0; [nums[j],nums[k]]=[nums[k],nums[j]]; } for (const n of nums) { if (valid(g,r,c,n)) { g[i]=n; if (solve(g,rand)) return true; g[i]=0; } } return false; } } return true; } function countSol(g, limit) { let cnt = 0; (function go(g) { if (cnt >= limit) return; for (let i = 0; i < 81; i++) { if (g[i] === 0) { const r = i/9|0, c = i%9; for (let n = 1; n <= 9; n++) { if (valid(g,r,c,n)) { g[i]=n; go(g); if (cnt>=limit) return; g[i]=0; } } return; } } cnt++; })([...g]); return cnt; } function generate(d) { const s = new Array(81).fill(0); solve(s, true); sol = [...s]; const p = [...s]; const rem = {easy:36, medium:44, hard:50, expert:56}[d]||44; const idx = [...Array(81).keys()]; for (let i = 80; i > 0; i--) { const j = Math.random()*(i+1)|0; [idx[i],idx[j]]=[idx[j],idx[i]]; } let removed = 0; for (const i of idx) { if (removed >= rem) break; const bak = p[i]; p[i] = 0; if (countSol(p, 2) === 1) removed++; else p[i] = bak; } return p; } /* ============ Rendering ============ */ function buildGrid() { const el = document.getElementById('grid'); el.innerHTML = ''; for (let i = 0; i < 81; i++) { const d = document.createElement('div'); d.className = 'cell'; d.dataset.i = i; d.setAttribute('role','gridcell'); d.tabIndex = -1; d.addEventListener('click', () => pick(i)); el.appendChild(d); } } function buildPad() { const el = document.getElementById('pad'); el.innerHTML = ''; for (let n = 1; n <= 9; n++) { const b = document.createElement('button'); b.className = 'num'; b.dataset.n = n; b.innerHTML = n + '<span class="cnt"></span>'; b.addEventListener('click', () => place(n)); el.appendChild(b); } } function render() { const cells = document.querySelectorAll('.cell'); const sr = sel !== null ? sel/9|0 : -1; const sc = sel !== null ? sel%9 : -1; const sb = sel !== null ? (sr/3|0)*3+(sc/3|0) : -1; const sn = sel !== null ? board[sel] : 0; cells.forEach((el, i) => { const r = i/9|0, c = i%9, b = (r/3|0)*3+(c/3|0); el.className = 'cell'; el.innerHTML = ''; if (fixed[i]) { el.classList.add('given'); el.textContent = board[i]; } else if (board[i]) { el.classList.add('user'); el.textContent = board[i]; if (board[i] !== sol[i]) el.classList.add('error'); } else if (notes[i] && notes[i].size) { const g = document.createElement('div'); g.className = 'notes-g'; for (let n = 1; n <= 9; n++) { const s = document.createElement('span'); s.textContent = notes[i].has(n) ? n : ''; g.appendChild(s); } el.appendChild(g); } if (sel !== null) { if (i === sel) el.classList.add('selected'); else if (r === sr || c === sc || b === sb) el.classList.add('hl'); if (sn && board[i] === sn && i !== sel) el.classList.add('same'); } }); updatePadCounts(); } function updatePadCounts() { const cnt = new Array(10).fill(0); for (let i = 0; i < 81; i++) if (board[i] && board[i] === sol[i]) cnt[board[i]]++; document.querySelectorAll('.num').forEach(b => { const n = +b.dataset.n, left = 9 - cnt[n]; b.querySelector('.cnt').textContent = left > 0 ? left : ''; b.classList.toggle('done', left === 0); }); } /* ============ Game Logic ============ */ function pick(i) { if (!over) { sel = i; render(); } } function place(n) { if (over || sel === null || fixed[sel]) return; if (noteMode) { const prev = new Set(notes[sel]); notes[sel].has(n) ? notes[sel].delete(n) : notes[sel].add(n); if (board[sel]) { hist.push({i:sel, v:board[sel], n:prev, t:'nc'}); board[sel] = 0; } else { hist.push({i:sel, v:0, n:prev, t:'n'}); } render(); return; } const pv = board[sel], pn = new Set(notes[sel]); if (board[sel] === n) { board[sel] = 0; hist.push({i:sel, v:n, n:pn, t:'p'}); render(); return; } board[sel] = n; notes[sel].clear(); hist.push({i:sel, v:pv, n:pn, t:'p'}); if (n !== sol[sel]) { errs++; document.getElementById('errCnt').textContent = errs; if (errs >= 3) { over = true; stopTimer(); toast('Game Over — Too many mistakes'); setTimeout(() => { for (let i=0;i<81;i++) if(!fixed[i]) board[i]=sol[i]; render(); }, 900); } } else { clearNotes(sel, n); } render(); const c = document.querySelectorAll('.cell')[sel]; c.classList.add('pop'); setTimeout(()=>c.classList.remove('pop'),220); if (!over) checkWin(); } function clearNotes(idx, n) { const r=idx/9|0, c=idx%9, br=(r/3|0)*3, bc=(c/3|0)*3; for (let i=0;i<9;i++) { notes[r*9+i].delete(n); notes[i*9+c].delete(n); } for (let dr=0;dr<3;dr++) for (let dc=0;dc<3;dc++) notes[(br+dr)*9+bc+dc].delete(n); } function erase() { if (over || sel===null || fixed[sel]) return; const pv=board[sel], pn=new Set(notes[sel]); if (!pv && !pn.size) return; hist.push({i:sel,v:pv,n:pn,t:'e'}); board[sel]=0; notes[sel].clear(); render(); } function undo() { if (over || !hist.length) return; const a=hist.pop(); board[a.i]=a.v; notes[a.i]=a.n; sel=a.i; render(); } function hint() { if (over) return; let idx = sel; if (idx===null || fixed[idx] || board[idx]===sol[idx]) { const cands=[]; for (let i=0;i<81;i++) if(!fixed[i] && board[i]!==sol[i]) cands.push(i); if (!cands.length) return; idx = cands[Math.random()*cands.length|0]; } hist.push({i:idx,v:board[idx],n:new Set(notes[idx]),t:'h'}); board[idx]=sol[idx]; notes[idx].clear(); fixed[idx]=true; sel=idx; clearNotes(idx,sol[idx]); render(); const c=document.querySelectorAll('.cell')[idx]; c.classList.add('pop'); setTimeout(()=>c.classList.remove('pop'),220); toast('Hint revealed'); checkWin(); } function checkWin() { for (let i=0;i<81;i++) if(board[i]!==sol[i]) return; over=true; stopTimer(); setTimeout(winScreen,280); } function winScreen() { document.getElementById('wTime').textContent=fmt(secs); document.getElementById('wDiff').textContent=diff[0].toUpperCase()+diff.slice(1); document.getElementById('wErr').textContent=errs; document.getElementById('winOvl').classList.add('show'); } /* ============ Timer ============ */ function startTimer() { stopTimer(); secs=0; updTimer(); tick=setInterval(()=>{secs++;updTimer();},1000); } function stopTimer() { if(tick){clearInterval(tick);tick=null;} } function updTimer() { document.getElementById('timer').textContent=fmt(secs); } function fmt(s) { return `${String(s/60|0).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; } /* ============ Toast ============ */ let toastT; function toast(m) { const el=document.getElementById('toast'); el.textContent=m; el.classList.add('show'); clearTimeout(toastT); toastT=setTimeout(()=>el.classList.remove('show'),2200); } /* ============ Theme ============ */ function toggleTheme() { const t = document.documentElement.dataset.theme==='dark'?'light':'dark'; document.documentElement.dataset.theme=t; localStorage.setItem('sdk-theme',t); document.getElementById('icoSun').style.display=t==='dark'?'block':'none'; document.getElementById('icoMoon').style.display=t==='light'?'block':'none'; } /* ============ New Game ============ */ function newGame(d) { diff=d||diff; over=false; errs=0; sel=null; noteMode=false; hist=[]; document.getElementById('errCnt').textContent='0'; document.getElementById('notesBtn').classList.remove('on'); document.getElementById('winOvl').classList.remove('show'); document.querySelectorAll('.diff-btn').forEach(b=>b.classList.toggle('active',b.dataset.d===diff)); const p=generate(diff); board=[...p]; fixed=p.map(v=>v!==0); notes=Array.from({length:81},()=>new Set()); render(); startTimer(); } /* ============ Keyboard ============ */ document.addEventListener('keydown', e => { if (over) return; if (e.key>='1' && e.key<='9') { e.preventDefault(); place(+e.key); return; } if (sel!==null && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) { e.preventDefault(); let r=sel/9|0, c=sel%9; if(e.key==='ArrowUp')r=Math.max(0,r-1); if(e.key==='ArrowDown')r=Math.min(8,r+1); if(e.key==='ArrowLeft')c=Math.max(0,c-1); if(e.key==='ArrowRight')c=Math.min(8,c+1); pick(r*9+c); return; } if(e.key==='Delete'||e.key==='Backspace'){e.preventDefault();erase();return;} if(e.key==='n'||e.key==='N'){e.preventDefault();toggleNotes();return;} if((e.key==='z'||e.key==='Z')&&(e.ctrlKey||e.metaKey)){e.preventDefault();undo();return;} if(e.key==='h'||e.key==='H'){e.preventDefault();hint();return;} }); function toggleNotes() { noteMode=!noteMode; document.getElementById('notesBtn').classList.toggle('on',noteMode); } /* ============ Init ============ */ (function init() { const saved=localStorage.getItem('sdk-theme')||'dark'; document.documentElement.dataset.theme=saved; document.getElementById('icoSun').style.display=saved==='dark'?'block':'none'; document.getElementById('icoMoon').style.display=saved==='light'?'block':'none'; buildGrid(); buildPad(); document.getElementById('themeBtn').addEventListener('click',toggleTheme); document.getElementById('undoBtn').addEventListener('click',undo); document.getElementById('eraseBtn').addEventListener('click',erase); document.getElementById('notesBtn').addEventListener('click',toggleNotes); document.getElementById('hintBtn').addEventListener('click',hint); document.getElementById('newBtn').addEventListener('click',()=>newGame()); document.getElementById('wNew').addEventListener('click',()=>newGame()); document.querySelectorAll('.diff-btn').forEach(b=>b.addEventListener('click',()=>newGame(b.dataset.d))); newGame('medium'); })(); </script> </body> </html>
Cancel