Chotto.app
Login
Sign Up
ES
App Name
kuyawa / memory
App Icon in SVG
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#4F46E5" /> <!-- Modern Indigo --> <stop offset="100%" stop-color="#22D3EE" /> <!-- Bright Teal --> </linearGradient> <filter id="smoothShadow" x="-20%" y="-20%" width="140%" height="140%"> <feDropShadow dx="0" dy="15" stdDeviation="15" flood-color="#000000" flood-opacity="0.25"/> </filter> </defs> <rect width="512" height="512" rx="115" fill="url(#bgGradient)" /> <g filter="url(#smoothShadow)" transform="rotate(-6, 195, 256)"> <rect x="105" y="130" width="180" height="252" rx="20" fill="white"/> <rect x="115" y="140" width="160" height="232" rx="15" fill="#EEF2F6"/> <path d="M195 190 L203.1 216.7 L230.9 221.7 L211.3 241.6 L216 269 L195 256 L174 269 L178.7 241.6 L159.1 221.7 L186.9 216.7 Z" fill="#FBBF24"/> </g> <g filter="url(#smoothShadow)" transform="rotate(6, 317, 256)"> <rect x="227" y="130" width="180" height="252" rx="20" fill="white" stroke="#D1D5DB" stroke-width="2"/> <rect x="237" y="140" width="160" height="232" rx="15" fill="#F9FAFB"/> <path d="M317 190 L325.1 216.7 L352.9 221.7 L333.3 241.6 L338 269 L317 256 L296 269 L300.7 241.6 L281.1 221.7 L308.9 216.7 Z" fill="#FBBF24"/> </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, maximum-scale=1.0, user-scalable=no" /> <title>Memory</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: radial-gradient(circle at 20% 20%, #0f172a, #020617); min-height: 100vh; display: flex; justify-content: center; align-items: center; font-family: 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', sans-serif; padding: 1rem; } .game-wrapper { background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(8px); border-radius: 48px; padding: 2rem 2rem 2.2rem; border: 1px solid rgba(255, 255, 255, 0.05); box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.9), inset 0 1px 2px rgba(255, 255, 255, 0.04); max-width: 750px; width: 100%; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; flex-wrap: wrap; gap: 0.6rem 1rem; } .title { font-size: 1.8rem; font-weight: 700; background: linear-gradient(135deg, #a78bfa, #8b5cf6, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.5px; text-shadow: 0 0 30px rgba(139, 92, 246, 0.15); } .stats { display: flex; align-items: center; gap: 1.2rem; background: rgba(0, 0, 0, 0.4); padding: 0.3rem 1.2rem 0.3rem 1.5rem; border-radius: 40px; border: 1px solid rgba(255, 255, 255, 0.04); } .stat-item { display: flex; align-items: baseline; gap: 0.3rem; } .stat-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1.2px; color: #94a3b8; font-weight: 600; } .stat-value { font-size: 1.5rem; font-weight: 700; color: #f1f5f9; min-width: 1.6rem; text-align: center; } .stat-value.moves { color: #fcd34d; } .stat-value.pairs { color: #6ee7b7; } .controls { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.8rem; margin-bottom: 1.2rem; } .btn-group { display: flex; gap: 0.6rem; flex-wrap: wrap; } .btn { background: rgba(30, 41, 59, 0.8); backdrop-filter: blur(2px); border: 1px solid rgba(255, 255, 255, 0.06); color: #e2e8f0; font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1.2rem; border-radius: 40px; cursor: pointer; transition: 0.2s ease; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); display: inline-flex; align-items: center; gap: 6px; letter-spacing: 0.3px; } .btn:hover { background: rgba(51, 65, 85, 0.9); color: white; transform: scale(0.96); border-color: rgba(167, 139, 250, 0.2); } .btn:active { transform: scale(0.92); } .btn-primary { background: linear-gradient(135deg, #8b5cf6, #7c3aed); color: white; border-color: #a78bfa; font-weight: 700; box-shadow: 0 4px 16px rgba(139, 92, 246, 0.25); } .btn-primary:hover { background: linear-gradient(135deg, #a78bfa, #8b5cf6); border-color: #c4b5fd; } .btn-icon { font-size: 1.1rem; line-height: 1; } .grid-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.7rem; aspect-ratio: 4 / 3; background: rgba(2, 6, 23, 0.4); border-radius: 28px; padding: 0.8rem; box-shadow: inset 0 8px 20px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.02); perspective: 1200px; position: relative; } .card { position: relative; aspect-ratio: 1 / 1; cursor: pointer; transform-style: preserve-3d; transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } .card.flipped { transform: rotateY(180deg); } .card.matched { transform: rotateY(180deg) scale(0.95); box-shadow: 0 0 30px rgba(110, 231, 183, 0.2); } .card-face { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 16px; display: flex; justify-content: center; align-items: center; font-size: 2.8rem; border: 2px solid rgba(255, 255, 255, 0.06); transition: box-shadow 0.3s ease; } .card-front { background: radial-gradient(circle at 30% 30%, #2d3748, #1a202c); transform: rotateY(0deg); border-color: rgba(255, 255, 255, 0.04); box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.05); } .card-front::after { content: '?'; font-size: 2.5rem; font-weight: 700; color: rgba(148, 163, 184, 0.2); letter-spacing: 2px; } .card-back { transform: rotateY(180deg); font-size: 3rem; border-color: rgba(255, 255, 255, 0.1); box-shadow: inset 0 2px 8px rgba(255, 255, 255, 0.05); } /* colorful back faces - expanded for 18 pairs */ .card-back.color-0 { background: radial-gradient(circle at 30% 30%, #f87171, #dc2626); } .card-back.color-1 { background: radial-gradient(circle at 30% 30%, #fb923c, #ea580c); } .card-back.color-2 { background: radial-gradient(circle at 30% 30%, #fcd34d, #d97706); } .card-back.color-3 { background: radial-gradient(circle at 30% 30%, #6ee7b7, #059669); } .card-back.color-4 { background: radial-gradient(circle at 30% 30%, #60a5fa, #2563eb); } .card-back.color-5 { background: radial-gradient(circle at 30% 30%, #a78bfa, #7c3aed); } .card-back.color-6 { background: radial-gradient(circle at 30% 30%, #f472b6, #db2777); } .card-back.color-7 { background: radial-gradient(circle at 30% 30%, #34d399, #059669); } .card-back.color-8 { background: radial-gradient(circle at 30% 30%, #fbbf24, #f59e0b); } .card-back.color-9 { background: radial-gradient(circle at 30% 30%, #f87171, #ef4444); } .card-back.color-10 { background: radial-gradient(circle at 30% 30%, #818cf8, #4f46e5); } .card-back.color-11 { background: radial-gradient(circle at 30% 30%, #f472b6, #ec4899); } .card-back.color-12 { background: radial-gradient(circle at 30% 30%, #34d399, #10b981); } .card-back.color-13 { background: radial-gradient(circle at 30% 30%, #fb923c, #f97316); } .card-back.color-14 { background: radial-gradient(circle at 30% 30%, #a78bfa, #8b5cf6); } .card-back.color-15 { background: radial-gradient(circle at 30% 30%, #60a5fa, #3b82f6); } .card-back.color-16 { background: radial-gradient(circle at 30% 30%, #fca5a5, #ef4444); } .card-back.color-17 { background: radial-gradient(circle at 30% 30%, #93c5fd, #3b82f6); } .card-back .emoji { filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); text-shadow: 0 2px 12px rgba(0, 0, 0, 0.2); } .footer-tip { margin-top: 0.8rem; text-align: center; color: #475569; font-size: 0.7rem; letter-spacing: 0.5px; opacity: 0.6; } .win-overlay { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(12px); border-radius: 48px; flex-direction: column; justify-content: center; align-items: center; z-index: 100; color: white; padding: 3rem 4rem; text-align: center; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8); min-width: 300px; } .win-overlay.show { display: flex; } .win-overlay h2 { font-size: 3rem; font-weight: 700; background: linear-gradient(135deg, #a78bfa, #fcd34d, #6ee7b7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.2rem; } .win-overlay p { opacity: 0.7; font-size: 1.1rem; margin-bottom: 1.8rem; } .win-overlay .btn { font-size: 1rem; padding: 0.7rem 2.4rem; } @media (max-width: 600px) { .game-wrapper { padding: 1rem; border-radius: 32px; } .title { font-size: 1.3rem; } .stats { padding: 0.2rem 0.8rem 0.2rem 1rem; gap: 0.8rem; } .stat-value { font-size: 1.2rem; min-width: 1.2rem; } .btn { padding: 0.3rem 0.9rem; font-size: 0.75rem; } .controls { flex-direction: column; align-items: stretch; } .btn-group { justify-content: center; } .grid-container { gap: 0.5rem; padding: 0.5rem; aspect-ratio: 4 / 3; } .card-back { font-size: 2rem; } .card-front::after { font-size: 1.8rem; } .win-overlay { padding: 2rem; min-width: 200px; width: 90%; } .win-overlay h2 { font-size: 2rem; } } @media (max-width: 400px) { .stat-label { font-size: 0.5rem; } .stat-value { font-size: 1rem; } .card-back { font-size: 1.6rem; } } </style> </head> <body> <div class="game-wrapper"> <div class="header"> <span class="title">๐ง MEMORY</span> <div class="stats"> <div class="stat-item"> <span class="stat-label">Moves</span> <span class="stat-value moves" id="movesDisplay">0</span> </div> <div class="stat-item"> <span class="stat-label">Pairs</span> <span class="stat-value pairs" id="pairsDisplay">0/8</span> </div> </div> </div> <div class="controls"> <div class="btn-group"> <button class="btn" id="resetBtn"><span class="btn-icon">โป</span> New</button> <button class="btn" id="difficultyBtn"><span class="btn-icon">โก</span> 4x4</button> </div> <button class="btn btn-primary" id="hintBtn">โจ Hint</button> </div> <div class="grid-container" id="gridContainer"> <!-- cards injected --> </div> <div class="footer-tip">click cards to flip ยท find all matching pairs</div> </div> <!-- Win overlay --> <div class="win-overlay" id="winOverlay"> <h2>๐ You Win!</h2> <p>completed in <span id="winMoves">0</span> moves</p> <button class="btn btn-primary" id="winResetBtn">Play again</button> </div> <script> (function() { // ---- state ---- const EMOJIS = [ '๐', '๐', '๐ฏ', 'โญ', '๐จ', '๐ฅ', '๐', '๐ต', '๐ฆ', '๐', '๐ฎ', 'โก', '๐ธ', '๐', '๐ญ', '๐งฉ', '๐', '๐ช', '๐ ', '๐ก', '๐ข', '๐ช', '๐', '๐', '๐', '๐ซ', 'โ๏ธ', '๐', 'โก', 'โ๏ธ', '๐ฅ', '๐ง' ]; let gridSize = 4; // 4x4 = 16 cards let cards = []; let flippedIndices = []; let matchedPairs = 0; let moves = 0; let isLocked = false; let timer = null; let gameWon = false; const gridContainer = document.getElementById('gridContainer'); const movesDisplay = document.getElementById('movesDisplay'); const pairsDisplay = document.getElementById('pairsDisplay'); const winOverlay = document.getElementById('winOverlay'); const winMoves = document.getElementById('winMoves'); const resetBtn = document.getElementById('resetBtn'); const difficultyBtn = document.getElementById('difficultyBtn'); const hintBtn = document.getElementById('hintBtn'); const winResetBtn = document.getElementById('winResetBtn'); let hintTimeout = null; // ---- helpers ---- function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function initCards() { const pairCount = (gridSize * gridSize) / 2; // Get unique emojis for the number of pairs needed const shuffledEmojis = shuffle([...EMOJIS]); const selectedEmojis = shuffledEmojis.slice(0, pairCount); let deck = []; selectedEmojis.forEach((emoji, index) => { deck.push({ emoji, id: index, matched: false }); deck.push({ emoji, id: index, matched: false }); }); return shuffle(deck); } // ---- game init ---- function resetGame() { if (timer) { clearTimeout(timer); timer = null; } if (hintTimeout) { clearTimeout(hintTimeout); hintTimeout = null; } cards = initCards(); flippedIndices = []; matchedPairs = 0; moves = 0; isLocked = false; gameWon = false; winOverlay.classList.remove('show'); updateUI(); renderGrid(); } function renderGrid() { gridContainer.innerHTML = ''; // Update grid columns based on size gridContainer.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; // Adjust aspect ratio for different sizes if (gridSize === 4) { gridContainer.style.aspectRatio = '4 / 3'; } else if (gridSize === 6) { gridContainer.style.aspectRatio = '1 / 1'; } else { gridContainer.style.aspectRatio = '1 / 1'; } cards.forEach((card, index) => { const cardEl = document.createElement('div'); cardEl.className = 'card'; cardEl.dataset.index = index; const front = document.createElement('div'); front.className = 'card-face card-front'; const back = document.createElement('div'); const colorClass = `color-${card.id % 18}`; // Support up to 18 pairs back.className = `card-face card-back ${colorClass}`; const emojiSpan = document.createElement('span'); emojiSpan.className = 'emoji'; emojiSpan.textContent = card.emoji; back.appendChild(emojiSpan); cardEl.appendChild(front); cardEl.appendChild(back); cardEl.addEventListener('click', () => handleCardClick(index)); gridContainer.appendChild(cardEl); }); } // ---- UI update ---- function updateUI() { movesDisplay.textContent = moves; const totalPairs = (gridSize * gridSize) / 2; pairsDisplay.textContent = `${matchedPairs}/${totalPairs}`; } // ---- card logic ---- function handleCardClick(index) { if (isLocked) return; if (gameWon) return; if (cards[index].matched) return; if (flippedIndices.includes(index)) return; // flip card flipCard(index, true); flippedIndices.push(index); if (flippedIndices.length === 2) { moves++; updateUI(); checkMatch(); } } function flipCard(index, addClass) { const cardsEl = gridContainer.querySelectorAll('.card'); const cardEl = cardsEl[index]; if (addClass) { cardEl.classList.add('flipped'); } else { cardEl.classList.remove('flipped'); } } function checkMatch() { isLocked = true; const [i1, i2] = flippedIndices; const card1 = cards[i1]; const card2 = cards[i2]; if (card1.id === card2.id && i1 !== i2) { console.log('match!') card1.matched = true; card2.matched = true; matchedPairs++; updateUI(); const cardsEl = gridContainer.querySelectorAll('.card'); cardsEl[i1].classList.add('matched'); cardsEl[i2].classList.add('matched'); flippedIndices = []; isLocked = false; if (matchedPairs === (gridSize * gridSize) / 2) { gameWon = true; winMoves.textContent = moves; winOverlay.classList.add('show'); } } else { console.log('no match') // no match: flip back after 3 seconds timer = setTimeout(() => { const cardsEl = gridContainer.querySelectorAll('.card'); // Only flip back if they haven't been matched //if (!cards[i1].matched && !flippedIndices.includes(i1)) { if (!card1.matched) { console.log('flip back', i1) flipCard(i1, false); } if (!card2.matched) { console.log('flip back', i2) flipCard(i2, false); } flippedIndices = []; isLocked = false; timer = null; }, 3000); // 3 seconds delay } } // ---- hint ---- function showHint() { if (gameWon) return; if (isLocked) return; if (hintTimeout) clearTimeout(hintTimeout); // find first unmatched card const unmatched = cards .map((c, i) => ({ ...c, index: i })) .filter(c => !c.matched && !flippedIndices.includes(c.index)); if (unmatched.length < 2) return; // find a pair for (let i = 0; i < unmatched.length; i++) { for (let j = i + 1; j < unmatched.length; j++) { if (unmatched[i].id === unmatched[j].id) { // flash these two const idx1 = unmatched[i].index; const idx2 = unmatched[j].index; const cardsEl = gridContainer.querySelectorAll('.card'); // briefly flip them flipCard(idx1, true); flipCard(idx2, true); hintTimeout = setTimeout(() => { // only flip back if they weren't matched in the meantime if (!cards[idx1].matched && !cards[idx2].matched && !flippedIndices.includes(idx1) && !flippedIndices.includes(idx2)) { flipCard(idx1, false); flipCard(idx2, false); } hintTimeout = null; }, 1500); return; } } } } // ---- difficulty toggle ---- function toggleDifficulty() { if (gridSize === 4) { gridSize = 6; // 6x6 = 36 cards difficultyBtn.innerHTML = '<span class="btn-icon">โก</span> 6x6'; } else { gridSize = 4; difficultyBtn.innerHTML = '<span class="btn-icon">โก</span> 4x4'; } resetGame(); } // ---- event listeners ---- resetBtn.addEventListener('click', resetGame); winResetBtn.addEventListener('click', resetGame); difficultyBtn.addEventListener('click', toggleDifficulty); hintBtn.addEventListener('click', showHint); // ---- start ---- resetGame(); })(); </script> </body> </html>
Cancel