Memory Match Adventure

by oktaviardi pratama

HTML

<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@400;600;700;800&display=swap" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">

    <!-- Floating background -->
    <div class="bg-shapes" id="bgShapes" aria-hidden="true"></div>

    <!-- Sound toggle -->
    <button class="sound-toggle" id="soundToggle" aria-label="Toggle sound">
        <i class="fas fa-volume-high"></i>
    </button>

    <!-- Toast -->
    <div class="toast-msg" id="toast" aria-live="polite"></div>

    <!-- ===== HERO SECTION ===== -->
    <section class="hero" id="heroSection" role="main">
        <!-- Decorative floating emojis -->
        <span class="deco" style="top:8%;left:10%;--dur:6s;animation-delay:-1s;">๐Ÿถ</span>
        <span class="deco" style="top:15%;right:12%;--dur:8s;animation-delay:-3s;">๐ŸŒˆ</span>
        <span class="deco" style="bottom:20%;left:8%;--dur:7s;animation-delay:-2s;">๐Ÿš€</span>
        <span class="deco" style="bottom:12%;right:10%;--dur:9s;animation-delay:-4s;">๐ŸŽˆ</span>
        <span class="deco" style="top:40%;left:5%;--dur:6.5s;animation-delay:-0.5s;">โญ</span>
        <span class="deco" style="top:55%;right:6%;--dur:7.5s;animation-delay:-2.5s;">๐ŸŽจ</span>

        <h1 class="hero-title">Memory Match</h1>
        <p class="hero-sub">Flip cards, find pairs, and train your brain</p>

        <div class="diff-cards" role="radiogroup" aria-label="Select difficulty">
            <button class="diff-card diff-easy selected" data-diff="easy" role="radio" aria-checked="true" tabindex="0">
                <span class="diff-label">Easy</span>
                <span class="diff-desc">4 x 3 Grid</span>
            </button>
            <button class="diff-card diff-medium" data-diff="medium" role="radio" aria-checked="false" tabindex="0">
                <span class="diff-label">Medium</span>
   ...

CSS

:root {
            --sky: #4FC3F7;
            --yellow: #FFD54F;
            --pink: #FF6B9D;
            --green: #66BB6A;
            --orange: #FF9800;
            --neon-blue: #00E5FF;
            --gold: #FFD700;
            --bg: #FFF8E1;
            --card-back: linear-gradient(135deg, #4FC3F7, #0288D1);
            --text: #2D3436;
            --text-light: #636E72;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Baloo 2', cursive, sans-serif;
            background: var(--bg);
            color: var(--text);
            overflow-x: hidden;
            min-height: 100vh;
        }

        /* Floating background shapes */
        .bg-shapes {
            position: fixed; inset: 0;
            pointer-events: none; z-index: 0; overflow: hidden;
        }
        .floating-shape {
            position: absolute; border-radius: 50%; opacity: 0.12;
            animation: floatShape var(--dur, 15s) ease-in-out infinite;
        }
        @keyframes floatShape {
            0%, 100% { transform: translateY(0) rotate(0deg) scale(1); }
            33% { transform: translateY(-35px) rotate(120deg) scale(1.08); }
            66% { transform: translateY(-18px) rotate(240deg) scale(0.94); }
        }

        /* Hero */
        .hero {
            position: relative; z-index: 1;
            display: flex; flex-direction: column; align-items: center;
            justify-content: center; min-height: 100vh;
            padding: 2rem; text-align: center;
        }
        .hero.hidden { display: none; }

        .hero-title {
            font-size: clamp(2.8rem, 8vw, 5.2rem);
            font-weight: 800; line-height: 1.1; margin-bottom: 0.3rem;
            background: linear-gradient(135deg, var(--sky), var(--green), var(--yellow), var(--pink));
            background-size: 300% 300%;
            -webkit-background-clip: text; -webkit-text-fill-color:...

JavaScript

/* ==============================
   CONFIGURATION & STATE
   ============================== */
const EMOJIS = [
    '๐Ÿถ','๐Ÿฑ','๐Ÿฐ','๐ŸฆŠ','๐Ÿป','๐Ÿผ','๐Ÿธ','๐Ÿฆ',
    '๐Ÿฏ','๐Ÿฎ','๐Ÿท','๐Ÿฆ„','๐Ÿ','๐ŸŒŸ','๐ŸŽ','๐Ÿ•',
    '๐ŸŽˆ','๐Ÿš€','๐ŸŽฏ','๐ŸŽจ','๐Ÿฐ','๐ŸŒˆ','๐ŸŽต','๐ŸŽช'
];

const DIFFICULTIES = {
    easy:   { cols: 4, rows: 3, pairs: 6,  label: 'Easy' },
    medium: { cols: 4, rows: 4, pairs: 8,  label: 'Medium' },
    hard:   { cols: 5, rows: 4, pairs: 10, label: 'Hard' }
};

let state = {
    difficulty: 'easy',
    cards: [],
    flippedIndices: [],
    matchedPairs: 0,
    totalPairs: 6,
    moves: 0,
    score: 0,
    combo: 0,
    timerSeconds: 0,
    timerInterval: null,
    isLocked: false,
    gameStarted: false
};

let audioCtx = null;
let soundEnabled = true;

/* ==============================
   AUDIO (Web Audio API)
   ============================== */
function initAudio() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();
}

function playSound(type) {
    if (!audioCtx || !soundEnabled) return;
    const now = audioCtx.currentTime;
    const masterGain = audioCtx.createGain();
    masterGain.connect(audioCtx.destination);

    switch (type) {
        case 'flip': {
            // Short percussive snap
            const osc = audioCtx.createOscillator();
            const g = audioCtx.createGain();
            osc.connect(g); g.connect(masterGain);
            osc.type = 'sine';
            osc.frequency.setValueAtTime(1200, now);
            osc.frequency.exponentialRampToValueAtTime(600, now + 0.08);
            g.gain.setValueAtTime(0.12, now);
            g.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
            osc.start(now); osc.stop(now + 0.08);
            break;
        }
        case 'match': {
            // Happy ascending arpeggio (C5-E5-G5)
            [523.25, 659.25, 783.99].forEach((freq, i)...