Created
April 22, 2026 19:03
-
-
Save senko/b7a291c277ad1f48b84d0900f483cf83 to your computer and use it in GitHub Desktop.
Qwen 3.6-27B Q4 zero-shotting Minesweeper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Minesweeper</title> | |
| <style> | |
| *, *::before, *::after { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg: #1a1a2e; | |
| --surface: #16213e; | |
| --surface2: #0f3460; | |
| --accent: #e94560; | |
| --accent2: #ff6b6b; | |
| --text: #eee; | |
| --text-dim: #8892b0; | |
| --cell-size: 36px; | |
| --cell-hidden: #2a3a5c; | |
| --cell-hidden-hover: #3a4f7a; | |
| --cell-revealed: #1a2540; | |
| --cell-border-light: #3a4f7a; | |
| --cell-border-dark: #1a2540; | |
| --gold: #f0c040; | |
| --green: #4ecdc4; | |
| } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| overflow-x: hidden; | |
| user-select: none; | |
| } | |
| h1 { | |
| font-size: 2.2rem; | |
| font-weight: 800; | |
| letter-spacing: 4px; | |
| text-transform: uppercase; | |
| margin-bottom: 20px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2), var(--gold)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-shadow: none; | |
| filter: drop-shadow(0 2px 8px rgba(233, 69, 96, 0.3)); | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 16px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .controls button { | |
| padding: 8px 20px; | |
| border: 2px solid var(--surface2); | |
| border-radius: 8px; | |
| background: var(--surface); | |
| color: var(--text-dim); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .controls button:hover { | |
| border-color: var(--accent); | |
| color: var(--text); | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 15px rgba(233, 69, 96, 0.2); | |
| } | |
| .controls button.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| box-shadow: 0 4px 20px rgba(233, 69, 96, 0.4); | |
| } | |
| .game-container { | |
| background: var(--surface); | |
| border-radius: 16px; | |
| padding: 20px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255,255,255,0.05); | |
| position: relative; | |
| } | |
| .status-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 16px; | |
| gap: 12px; | |
| } | |
| .counter { | |
| background: #0a0f1e; | |
| border: 2px solid #1a2540; | |
| border-radius: 8px; | |
| padding: 6px 14px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--accent); | |
| min-width: 70px; | |
| text-align: center; | |
| letter-spacing: 2px; | |
| box-shadow: inset 0 2px 8px rgba(0,0,0,0.5); | |
| } | |
| .face-btn { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| border: 3px solid var(--surface2); | |
| background: linear-gradient(145deg, #2a3a5c, #1a2540); | |
| cursor: pointer; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| } | |
| .face-btn:hover { | |
| transform: scale(1.1); | |
| border-color: var(--accent); | |
| } | |
| .face-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .board { | |
| display: inline-grid; | |
| gap: 2px; | |
| background: #0a0f1e; | |
| border-radius: 8px; | |
| padding: 2px; | |
| border: 2px solid #1a2540; | |
| } | |
| .cell { | |
| width: var(--cell-size); | |
| height: var(--cell-size); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 0.95rem; | |
| font-weight: 800; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: all 0.1s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .cell.hidden { | |
| background: linear-gradient(145deg, #2e4068, #243356); | |
| border: 1px solid #3a5080; | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 3px rgba(0,0,0,0.2); | |
| } | |
| .cell.hidden:hover { | |
| background: linear-gradient(145deg, #3a5080, #2e4068); | |
| border-color: #4a6090; | |
| transform: scale(1.05); | |
| z-index: 1; | |
| } | |
| .cell.hidden:active { | |
| transform: scale(0.95); | |
| box-shadow: inset 0 2px 6px rgba(0,0,0,0.4); | |
| } | |
| .cell.revealed { | |
| background: var(--cell-revealed); | |
| border: 1px solid rgba(255,255,255,0.03); | |
| cursor: default; | |
| } | |
| .cell.flagged { | |
| background: linear-gradient(145deg, #2e4068, #243356); | |
| border: 1px solid #3a5080; | |
| animation: flagPop 0.3s ease; | |
| } | |
| .cell.mine-hit { | |
| background: var(--accent) !important; | |
| border-color: var(--accent) !important; | |
| animation: explode 0.4s ease; | |
| } | |
| .cell.mine-revealed { | |
| background: #1a1a2e; | |
| border: 1px solid rgba(233, 69, 96, 0.3); | |
| } | |
| .cell.wrong-flag { | |
| background: #2a1a1a; | |
| border: 1px solid rgba(233, 69, 96, 0.5); | |
| } | |
| @keyframes flagPop { | |
| 0% { transform: scale(0.5) rotate(-20deg); opacity: 0; } | |
| 60% { transform: scale(1.2) rotate(5deg); } | |
| 100% { transform: scale(1) rotate(0deg); opacity: 1; } | |
| } | |
| @keyframes explode { | |
| 0% { transform: scale(1); } | |
| 30% { transform: scale(1.3); } | |
| 100% { transform: scale(1); } | |
| } | |
| @keyframes revealCell { | |
| 0% { transform: scale(0.8); opacity: 0; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| .cell.revealed { | |
| animation: revealCell 0.15s ease; | |
| } | |
| .cell .flag-icon { | |
| font-size: 1.1rem; | |
| filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); | |
| } | |
| .cell .mine-icon { | |
| font-size: 1.1rem; | |
| } | |
| .n1 { color: #4fc3f7; } | |
| .n2 { color: #66bb6a; } | |
| .n3 { color: #ef5350; } | |
| .n4 { color: #ab47bc; } | |
| .n5 { color: #ff7043; } | |
| .n6 { color: #26c6da; } | |
| .n7 { color: #ec407a; } | |
| .n8 { color: #bdbdbd; } | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(10, 15, 30, 0.85); | |
| border-radius: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| backdrop-filter: blur(4px); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.4s ease; | |
| } | |
| .overlay.show { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .overlay-title { | |
| font-size: 2rem; | |
| font-weight: 800; | |
| margin-bottom: 8px; | |
| letter-spacing: 2px; | |
| } | |
| .overlay-title.win { | |
| background: linear-gradient(135deg, var(--gold), var(--green)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .overlay-title.lose { | |
| color: var(--accent); | |
| } | |
| .overlay-sub { | |
| color: var(--text-dim); | |
| font-size: 0.95rem; | |
| margin-bottom: 20px; | |
| } | |
| .overlay button { | |
| padding: 10px 30px; | |
| border-radius: 8px; | |
| border: none; | |
| background: var(--accent); | |
| color: #fff; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .overlay button:hover { | |
| background: var(--accent2); | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(233, 69, 96, 0.4); | |
| } | |
| .info-bar { | |
| display: flex; | |
| justify-content: center; | |
| gap: 20px; | |
| margin-top: 14px; | |
| color: var(--text-dim); | |
| font-size: 0.8rem; | |
| letter-spacing: 1px; | |
| } | |
| .info-bar span { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .confetti-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 100; | |
| overflow: hidden; | |
| } | |
| .confetti { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| top: -10px; | |
| animation: confettiFall linear forwards; | |
| } | |
| @keyframes confettiFall { | |
| 0% { transform: translateY(0) rotate(0deg); opacity: 1; } | |
| 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; } | |
| } | |
| @media (max-width: 600px) { | |
| :root { --cell-size: 28px; } | |
| h1 { font-size: 1.5rem; } | |
| .game-container { padding: 12px; } | |
| .counter { font-size: 1.2rem; min-width: 55px; padding: 4px 10px; } | |
| .face-btn { width: 40px; height: 40px; font-size: 1.2rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>π£ Minesweeper</h1> | |
| <div class="controls"> | |
| <button data-difficulty="beginner" class="active">Beginner</button> | |
| <button data-difficulty="intermediate">Intermediate</button> | |
| <button data-difficulty="expert">Expert</button> | |
| </div> | |
| <div class="game-container"> | |
| <div class="status-bar"> | |
| <div class="counter" id="mineCounter">000</div> | |
| <button class="face-btn" id="faceBtn">π</button> | |
| <div class="counter" id="timer">000</div> | |
| </div> | |
| <div class="board" id="board"></div> | |
| <div class="overlay" id="overlay"> | |
| <div class="overlay-title" id="overlayTitle"></div> | |
| <div class="overlay-sub" id="overlaySub"></div> | |
| <button id="overlayBtn">Play Again</button> | |
| </div> | |
| </div> | |
| <div class="info-bar"> | |
| <span>π±οΈ Left click: Reveal</span> | |
| <span>π© Right click: Flag</span> | |
| <span>π Middle: Chord</span> | |
| </div> | |
| <div class="confetti-container" id="confettiContainer"></div> | |
| <script> | |
| const DIFFICULTIES = { | |
| beginner: { rows: 9, cols: 9, mines: 10 }, | |
| intermediate: { rows: 16, cols: 16, mines: 40 }, | |
| expert: { rows: 16, cols: 30, mines: 99 } | |
| }; | |
| let difficulty = 'beginner'; | |
| let rows, cols, totalMines; | |
| let board = []; // 2D array of cell data | |
| let revealed = []; | |
| let flagged = []; | |
| let mineMap = []; | |
| let gameOver = false; | |
| let gameStarted = false; | |
| let firstClick = true; | |
| let flagCount = 0; | |
| let timerInterval = null; | |
| let seconds = 0; | |
| let revealedCount = 0; | |
| let cellsRevealed = 0; | |
| const boardEl = document.getElementById('board'); | |
| const mineCounterEl = document.getElementById('mineCounter'); | |
| const timerEl = document.getElementById('timer'); | |
| const faceBtn = document.getElementById('faceBtn'); | |
| const overlay = document.getElementById('overlay'); | |
| const overlayTitle = document.getElementById('overlayTitle'); | |
| const overlaySub = document.getElementById('overlaySub'); | |
| const overlayBtn = document.getElementById('overlayBtn'); | |
| const confettiContainer = document.getElementById('confettiContainer'); | |
| // Difficulty buttons | |
| document.querySelectorAll('.controls button').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelector('.controls button.active').classList.remove('active'); | |
| btn.classList.add('active'); | |
| difficulty = btn.dataset.difficulty; | |
| initGame(); | |
| }); | |
| }); | |
| faceBtn.addEventListener('click', initGame); | |
| overlayBtn.addEventListener('click', initGame); | |
| function initGame() { | |
| const cfg = DIFFICULTIES[difficulty]; | |
| rows = cfg.rows; | |
| cols = cfg.cols; | |
| totalMines = cfg.mines; | |
| board = []; | |
| revealed = []; | |
| flagged = []; | |
| mineMap = []; | |
| gameOver = false; | |
| gameStarted = false; | |
| firstClick = true; | |
| flagCount = 0; | |
| revealedCount = 0; | |
| cellsRevealed = 0; | |
| seconds = 0; | |
| clearInterval(timerInterval); | |
| timerInterval = null; | |
| faceBtn.textContent = 'π'; | |
| overlay.classList.remove('show'); | |
| confettiContainer.innerHTML = ''; | |
| for (let r = 0; r < rows; r++) { | |
| board[r] = []; | |
| revealed[r] = []; | |
| flagged[r] = []; | |
| mineMap[r] = []; | |
| for (let c = 0; c < cols; c++) { | |
| board[r][c] = 0; | |
| revealed[r][c] = false; | |
| flagged[r][c] = false; | |
| mineMap[r][c] = false; | |
| } | |
| } | |
| updateMineCounter(); | |
| timerEl.textContent = '000'; | |
| renderBoard(); | |
| } | |
| function placeMines(safeRow, safeCol) { | |
| // Create safe zone around first click | |
| const safeZone = new Set(); | |
| for (let dr = -1; dr <= 1; dr++) { | |
| for (let dc = -1; dc <= 1; dc++) { | |
| const nr = safeRow + dr; | |
| const nc = safeCol + dc; | |
| if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) { | |
| safeZone.add(nr * cols + nc); | |
| } | |
| } | |
| } | |
| let placed = 0; | |
| while (placed < totalMines) { | |
| const r = Math.floor(Math.random() * rows); | |
| const c = Math.floor(Math.random() * cols); | |
| if (!mineMap[r][c] && !safeZone.has(r * cols + c)) { | |
| mineMap[r][c] = true; | |
| placed++; | |
| } | |
| } | |
| // Calculate numbers | |
| for (let r = 0; r < rows; r++) { | |
| for (let c = 0; c < cols; c++) { | |
| if (mineMap[r][c]) { | |
| board[r][c] = -1; | |
| } else { | |
| let count = 0; | |
| forNeighbors(r, c, (nr, nc) => { | |
| if (mineMap[nr][nc]) count++; | |
| }); | |
| board[r][c] = count; | |
| } | |
| } | |
| } | |
| } | |
| function forNeighbors(r, c, fn) { | |
| for (let dr = -1; dr <= 1; dr++) { | |
| for (let dc = -1; dc <= 1; dc++) { | |
| if (dr === 0 && dc === 0) continue; | |
| const nr = r + dr; | |
| const nc = c + dc; | |
| if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) { | |
| fn(nr, nc); | |
| } | |
| } | |
| } | |
| } | |
| function renderBoard() { | |
| boardEl.innerHTML = ''; | |
| boardEl.style.gridTemplateColumns = `repeat(${cols}, var(--cell-size))`; | |
| boardEl.style.gridTemplateRows = `repeat(${rows}, var(--cell-size))`; | |
| for (let r = 0; r < rows; r++) { | |
| for (let c = 0; c < cols; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell hidden'; | |
| cell.dataset.row = r; | |
| cell.dataset.col = c; | |
| cell.addEventListener('mousedown', onCellMouseDown); | |
| cell.addEventListener('contextmenu', e => e.preventDefault()); | |
| cell.addEventListener('touchstart', onTouchStart, { passive: false }); | |
| cell.addEventListener('touchend', onTouchEnd, { passive: false }); | |
| cell.addEventListener('touchmove', onTouchMove, { passive: false }); | |
| boardEl.appendChild(cell); | |
| } | |
| } | |
| } | |
| let touchTimer = null; | |
| let touchMoved = false; | |
| let touchStartX, touchStartY; | |
| function onTouchStart(e) { | |
| if (gameOver) return; | |
| const touch = e.touches[0]; | |
| touchStartX = touch.clientX; | |
| touchStartY = touch.clientY; | |
| touchMoved = false; | |
| touchTimer = setTimeout(() => { | |
| const r = parseInt(e.target.dataset.row); | |
| const c = parseInt(e.target.dataset.col); | |
| toggleFlag(r, c); | |
| touchTimer = null; | |
| }, 400); | |
| } | |
| function onTouchMove(e) { | |
| touchMoved = true; | |
| if (touchTimer) { | |
| clearTimeout(touchTimer); | |
| touchTimer = null; | |
| } | |
| } | |
| function onTouchEnd(e) { | |
| if (touchMoved) return; | |
| e.preventDefault(); | |
| if (touchTimer) { | |
| clearTimeout(touchTimer); | |
| touchTimer = null; | |
| return; | |
| } | |
| const r = parseInt(e.target.dataset.row); | |
| const c = parseInt(e.target.dataset.col); | |
| if (r !== undefined && !isNaN(r)) { | |
| revealCell(r, c); | |
| } | |
| } | |
| function onCellMouseDown(e) { | |
| e.preventDefault(); | |
| if (gameOver) return; | |
| const r = parseInt(e.target.dataset.row); | |
| const c = parseInt(e.target.dataset.col); | |
| if (e.button === 0) { | |
| // Left click | |
| if (revealed[r][c] && !flagged[r][c]) { | |
| // Chord: if cell is revealed and neighbors flagged == number, reveal rest | |
| chordReveal(r, c); | |
| } else if (!flagged[r][c]) { | |
| revealCell(r, c); | |
| } | |
| } else if (e.button === 2) { | |
| // Right click | |
| toggleFlag(r, c); | |
| } else if (e.button === 1) { | |
| // Middle click = chord | |
| chordReveal(r, c); | |
| } | |
| } | |
| function toggleFlag(r, c) { | |
| if (gameOver || revealed[r][c]) return; | |
| const cell = getCellEl(r, c); | |
| if (flagged[r][c]) { | |
| flagged[r][c] = false; | |
| flagCount--; | |
| cell.className = 'cell hidden'; | |
| cell.innerHTML = ''; | |
| } else { | |
| flagged[r][c] = true; | |
| flagCount++; | |
| cell.className = 'cell hidden flagged'; | |
| cell.innerHTML = '<span class="flag-icon">π©</span>'; | |
| } | |
| updateMineCounter(); | |
| } | |
| function chordReveal(r, c) { | |
| if (!revealed[r][c] || board[r][c] <= 0) return; | |
| let adjFlags = 0; | |
| forNeighbors(r, c, (nr, nc) => { | |
| if (flagged[nr][nc]) adjFlags++; | |
| }); | |
| if (adjFlags === board[r][c]) { | |
| forNeighbors(r, c, (nr, nc) => { | |
| if (!revealed[nr][nc] && !flagged[nr][nc]) { | |
| revealCell(nr, nc); | |
| } | |
| }); | |
| } | |
| } | |
| function revealCell(r, c) { | |
| if (gameOver || revealed[r][c] || flagged[r][c]) return; | |
| if (firstClick) { | |
| firstClick = false; | |
| placeMines(r, c); | |
| startTimer(); | |
| } | |
| revealed[r][c] = true; | |
| cellsRevealed++; | |
| const cell = getCellEl(r, c); | |
| if (mineMap[r][c]) { | |
| // Hit a mine | |
| gameOver = true; | |
| cell.className = 'cell revealed mine-hit'; | |
| cell.innerHTML = '<span class="mine-icon">π£</span>'; | |
| faceBtn.textContent = 'π΅'; | |
| revealAllMines(r, c); | |
| stopTimer(); | |
| setTimeout(() => showOverlay(false), 600); | |
| return; | |
| } | |
| cell.className = 'cell revealed'; | |
| if (board[r][c] > 0) { | |
| cell.textContent = board[r][c]; | |
| cell.classList.add('n' + board[r][c]); | |
| } else { | |
| // Flood fill for empty cells | |
| cell.textContent = ''; | |
| forNeighbors(r, c, (nr, nc) => { | |
| if (!revealed[nr][nc] && !flagged[nr][nc]) { | |
| revealCell(nr, nc); | |
| } | |
| }); | |
| } | |
| // Check win | |
| if (cellsRevealed === rows * cols - totalMines) { | |
| gameOver = true; | |
| faceBtn.textContent = 'π'; | |
| stopTimer(); | |
| autoFlagRemaining(); | |
| setTimeout(() => showOverlay(true), 400); | |
| } | |
| } | |
| function revealAllMines(hitR, hitC) { | |
| for (let r = 0; r < rows; r++) { | |
| for (let c = 0; c < cols; c++) { | |
| const cell = getCellEl(r, c); | |
| if (mineMap[r][c] && !flagged[r][c]) { | |
| if (r === hitR && c === hitC) { | |
| // Already shown | |
| } else { | |
| setTimeout(() => { | |
| cell.className = 'cell revealed mine-revealed'; | |
| cell.innerHTML = '<span class="mine-icon">π£</span>'; | |
| }, Math.random() * 500); | |
| } | |
| } else if (flagged[r][c] && !mineMap[r][c]) { | |
| setTimeout(() => { | |
| cell.className = 'cell revealed wrong-flag'; | |
| cell.innerHTML = '<span style="font-size:1.1rem">β</span>'; | |
| }, Math.random() * 500); | |
| } | |
| } | |
| } | |
| } | |
| function autoFlagRemaining() { | |
| for (let r = 0; r < rows; r++) { | |
| for (let c = 0; c < cols; c++) { | |
| if (mineMap[r][c] && !flagged[r][c]) { | |
| flagged[r][c] = true; | |
| flagCount++; | |
| const cell = getCellEl(r, c); | |
| cell.className = 'cell hidden flagged'; | |
| cell.innerHTML = '<span class="flag-icon">π©</span>'; | |
| } | |
| } | |
| } | |
| updateMineCounter(); | |
| } | |
| function getCellEl(r, c) { | |
| return boardEl.children[r * cols + c]; | |
| } | |
| function updateMineCounter() { | |
| const val = totalMines - flagCount; | |
| mineCounterEl.textContent = String(Math.max(val, 0)).padStart(3, '0'); | |
| if (val < 0) mineCounterEl.textContent = '-' + String(Math.abs(val)).padStart(2, '0'); | |
| } | |
| function startTimer() { | |
| if (timerInterval) return; | |
| gameStarted = true; | |
| timerInterval = setInterval(() => { | |
| seconds++; | |
| if (seconds > 999) seconds = 999; | |
| timerEl.textContent = String(seconds).padStart(3, '0'); | |
| }, 1000); | |
| } | |
| function stopTimer() { | |
| clearInterval(timerInterval); | |
| timerInterval = null; | |
| } | |
| function showOverlay(won) { | |
| overlayTitle.className = 'overlay-title ' + (won ? 'win' : 'lose'); | |
| overlayTitle.textContent = won ? 'π You Win!' : 'π₯ Game Over'; | |
| overlaySub.textContent = won | |
| ? `Cleared in ${seconds} second${seconds !== 1 ? 's' : ''}` | |
| : 'Better luck next time!'; | |
| overlay.classList.add('show'); | |
| if (won) { | |
| launchConfetti(); | |
| } | |
| } | |
| function launchConfetti() { | |
| const colors = ['#e94560', '#f0c040', '#4ecdc4', '#4fc3f7', '#ab47bc', '#ff7043', '#66bb6a']; | |
| for (let i = 0; i < 80; i++) { | |
| setTimeout(() => { | |
| const confetti = document.createElement('div'); | |
| confetti.className = 'confetti'; | |
| confetti.style.left = Math.random() * 100 + '%'; | |
| confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; | |
| confetti.style.width = (Math.random() * 8 + 4) + 'px'; | |
| confetti.style.height = (Math.random() * 8 + 4) + 'px'; | |
| confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '2px'; | |
| confetti.style.animationDuration = (Math.random() * 2 + 1.5) + 's'; | |
| confetti.style.animationDelay = '0s'; | |
| confettiContainer.appendChild(confetti); | |
| setTimeout(() => confetti.remove(), 4000); | |
| }, i * 30); | |
| } | |
| } | |
| // Prevent context menu on board | |
| boardEl.addEventListener('contextmenu', e => e.preventDefault()); | |
| // Init | |
| initGame(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment