آموزش ساخت بازی حدس عدد با جاوااسکریپت و حافظه محلی مرورگر

آموزش ساخت بازی جذاب و تفریحی حدس عدد با جاوااسکریپت خالص، طراحی واکنش‌گرا، انیمیشن‌های CSS و ثبت رکورد در حافظه مرورگر به همراه کد کامل و یکپارچه.

🎧 خلاصه صوتی مقاله (پادکست ۲ دقیقه‌ای):

فرصت مطالعه ندارید؟ چکیده این مطلب را همین‌جا گوش دهید:

یادگیری برنامه‌نویسی وب زمانی جذاب می‌شود که بتوانید دانش نظری خود را به یک محصول ملموس، تعاملی و سرگرم‌کننده تبدیل کنید. بسیاری از افراد تازه‌کار در شروع مسیر با چالش ترکیب مفاهیم مختلف مواجه هستند. یکی از بهترین روش‌ها برای عبور از این چالش، پیاده‌سازی مینی‌پروژه‌های کاربردی است. در این مقاله قصد داریم یک بازی کلاسیک حدس عدد (Number Guessing Game) را با ظاهری مدرن، انیمیشن‌های روان و قابلیت ثبت بهترین رکورد با استفاده از حافظه مرورگر از صفر تا صد پیاده‌سازی کنیم. این پروژه به‌صورت یکپارچه در یک فایل منفرد آماده شده است تا بتوانید بدون نیاز به ابزارهای پیچیده، آن را اجرا کنید.

چرا ساخت بازی حدس عدد یک تمرین عالی برای جاوااسکریپت است؟

بازی حدس عدد فراتر از یک سرگرمی ساده، تمام ارکان اصلی برنامه‌نویسی فرانت‌اند (Front-end) را به چالش می‌کشد. شما در این پروژه با تولید اعداد تصادفی امن، دستکاری عناصر صفحه (DOM Manipulation)، مدیریت رویدادها (Event Listeners) و کنترل شرایط منطقی سر و کار خواهید داشت. همچنین برای ارتقای تجربه کاربری، از قابلیت‌های مدرن CSS برای ایجاد انیمیشن‌های لرزشی در زمان خطای کاربر و افکت‌های بصری جذاب برای پیروزی استفاده می‌کنیم. در کنار این موارد، مشابه پروژه‌های پیشرفته‌تر، یاد می‌گیرید چگونه با استفاده از فضای ذخیره‌سازی محلی (localStorage)، اطلاعات بازی را ماندگار کنید تا با بستن مرورگر، رکورد کاربر پاک نشود.

اگر به طراحی رابط‌های کاربری علاقه دارید، می‌توانید در کنار این پروژه به ابزارهایی مانند راهنمای استفاده از Bootstrap Icons؛ گالری آیکون رایگان و متن‌باز سر بزنید تا با آیکون‌های متنوع برای زیباتر کردن دکمه‌ها آشنا شوید. در ادامه، کدهای کامل این پروژه را بررسی خواهیم کرد.

مشخصات پروژهجزئیات فنی
فناوری‌های استفاده شدهHTML5, CSS3, Vanilla JavaScript
ساختار فایلتک‌فایلی (Single-File) بدون نیاز به نصب پکیج یا ابزار جانبی
قابلیت ذخیره‌سازیپشتیبانی از localStorage برای ثبت بهترین رکورد (High Score)
مدیریت دسترسی ایران۱۰۰٪ آزاد و بدون محدودیت (اجرا روی مرورگر آفلاین)

کد کامل و یکپارچه‌ی پروژه (HTML, CSS, JS)

برای اجرای این بازی، کافی است کد زیر را در یک فایل متنی کپی کرده و آن را با پسوند .html (مثلاً game.html) ذخیره کنید. سپس فایل را با هر مرورگر وب مدرنی باز کنید تا بازی بلافاصله اجرا شود. هیچ کتابخانه خارجی یا اتصال به اینترنتی برای اجرای این کد نیاز نیست.

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>بازی حرفه‌ای حدس عدد</title>
    <style>
        :root {
            --bg-color: #0f172a;
            --card-bg: #1e293b;
            --text-color: #f8fafc;
            --primary: #6366f1;
            --primary-hover: #4f46e5;
            --accent: #22c55e;
            --danger: #ef4444;
            --border: #334155;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }

        body {
            background-color: var(--bg-color);
            color: var(--text-color);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .game-container {
            background-color: var(--card-bg);
            border: 1px solid var(--border);
            border-radius: 16px;
            padding: 30px;
            width: 100%;
            max-width: 420px;
            box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
            text-align: center;
        }

        h1 {
            font-size: 1.5rem;
            margin-bottom: 10px;
            color: var(--text-color);
        }

        p.description {
            font-size: 0.9rem;
            color: #94a3b8;
            margin-bottom: 25px;
        }

        .stats-box {
            display: flex;
            justify-content: space-around;
            background: rgba(255, 255, 255, 0.03);
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 20px;
            font-size: 0.85rem;
            border: 1px solid var(--border);
        }

        .stats-box span {
            font-weight: bold;
            color: var(--primary);
        }

        .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        input[type="number"] {
            flex: 1;
            background: #0f172a;
            border: 1px solid var(--border);
            border-radius: 8px;
            padding: 12px 15px;
            color: white;
            font-size: 1rem;
            text-align: center;
            outline: none;
            transition: border-color 0.2s;
        }

        input[type="number"]:focus {
            border-color: var(--primary);
        }

        button {
            background-color: var(--primary);
            color: white;
            border: none;
            border-radius: 8px;
            padding: 12px 20px;
            font-size: 1rem;
            font-weight: bold;
            cursor: pointer;
            transition: background-color 0.2s, transform 0.1s;
        }

        button:hover {
            background-color: var(--primary-hover);
        }

        button:active {
            transform: scale(0.98);
        }

        .message {
            font-size: 0.95rem;
            margin-bottom: 20px;
            min-height: 24px;
            font-weight: 500;
        }

        .message.success { color: var(--accent); }
        .message.error { color: var(--danger); }
        .message.hint { color: #f59e0b; }

        .restart-btn {
            background-color: transparent;
            border: 1px solid var(--border);
            color: #94a3b8;
            width: 100%;
            display: none;
        }

        .restart-btn:hover {
            background-color: rgba(255, 255, 255, 0.05);
            color: white;
            border-color: #94a3b8;
        }

        @keyframes shake {
            0% { transform: translateX(0); }
            25% { transform: translateX(-5px); }
            50% { transform: translateX(5px); }
            75% { transform: translateX(-5px); }
            100% { transform: translateX(0); }
        }

        .shake {
            animation: shake 0.3s ease-in-out;
        }
    </style>
</head>
<body>

    <div class="game-container" id="gameCard">
        <h1>بازی حدس عدد</h1>
        <p class="description">یک عدد بین ۱ تا ۱۰۰ انتخاب کرده‌ام. حدس بزنید چیست؟</p>

        <div class="stats-box">
            <div>تلاش‌ها: <span id="attemptsCount">0</span></div>
            <div>بهترین رکورد: <span id="highScore">-</span></div>
        </div>

        <div class="input-group">
            <input type="number" id="userGuess" min="1" max="100" placeholder="عدد خود را وارد کنید...">
            <button id="guessBtn">بررسی</button>
        </div>

        <div class="message" id="messageArea"></div>

        <button class="restart-btn" id="restartBtn">شروع مجدد بازی</button>
    </div>

    <script>
        // متغیرهای اصلی وضعیت بازی
        let secretNumber = Math.floor(Math.random() * 100) + 1;
        let attempts = 0;
        let isGameOver = false;

        // انتخاب عناصر از سند HTML
        const userGuessInput = document.getElementById('userGuess');
        const guessBtn = document.getElementById('guessBtn');
        const messageArea = document.getElementById('messageArea');
        const attemptsCountDisplay = document.getElementById('attemptsCount');
        const highScoreDisplay = document.getElementById('highScore');
        const restartBtn = document.getElementById('restartBtn');
        const gameCard = document.getElementById('gameCard');

        // بازیابی بهترین رکورد از localStorage
        let savedHighScore = localStorage.getItem('number_game_highscore');
        if (savedHighScore !== null) {
            highScoreDisplay.textContent = savedHighScore;
        }

        // تابع اصلی پردازش حدس کاربر
        function handleGuess() {
            if (isGameOver) return;

            const userVal = parseInt(userGuessInput.value);

            // اعتبارسنجی ورودی کاربر
            if (isNaN(userVal) || userVal < 1 || userVal > 100) {
                showMessage('لطفاً یک عدد معتبر بین ۱ تا ۱۰۰ وارد کنید.', 'error');
                triggerShake();
                return;
            }

            attempts++;
            attemptsCountDisplay.textContent = attempts;

            // بررسی نتیجه حدس
            if (userVal === secretNumber) {
                // حالت برد
                isGameOver = true;
                showMessage('تبریک! عدد درست را پیدا کردید. تعداد تلاش: ' + attempts, 'success');
                
                // مدیریت بهترین رکورد
                checkAndUpdateHighScore(attempts);

                // تغییر وضعیت دکمه‌ها
                guessBtn.style.display = 'none';
                userGuessInput.style.display = 'none';
                restartBtn.style.display = 'block';

            } else if (userVal < secretNumber) {
                showMessage('عدد مخفی بزرگ‌تر است! 📈', 'hint');
                triggerShake();
            } else {
                showMessage('عدد مخفی کوچک‌تر است! 📉', 'hint');
                triggerShake();
            }

            userGuessInput.value = '';
            userGuessInput.focus();
        }

        // تابع نمایش پیام‌ها با استایل‌های مختلف
        function showMessage(text, type) {
            messageArea.textContent = text;
            messageArea.className = 'message ' + type;
        }

        // تابع ایجاد انیمیشن لرزش هنگام خطا
        function triggerShake() {
            gameCard.classList.add('shake');
            setTimeout(() => {
                gameCard.classList.remove('shake');
            }, 300);
        }

        // بررسی و ذخیره رکورد در حافظه مرورگر
        function checkAndUpdateHighScore(currentAttempts) {
            let currentBest = localStorage.getItem('number_game_highscore');
            
            if (currentBest === null || currentAttempts < parseInt(currentBest)) {
                localStorage.setItem('number_game_highscore', currentAttempts);
                highScoreDisplay.textContent = currentAttempts;
                showMessage('تبریک! شما رکورد جدیدی ثبت کردید: ' + currentAttempts + ' تلاش', 'success');
            }
        }

        // ریست کردن بازی برای دور جدید
        function resetGame() {
            secretNumber = Math.floor(Math.random() * 100) + 1;
            attempts = 0;
            isGameOver = false;

            attemptsCountDisplay.textContent = '0';
            messageArea.textContent = '';
            messageArea.className = 'message';

            guessBtn.style.display = 'block';
            userGuessInput.style.display = 'block';
            restartBtn.style.display = 'none';

            userGuessInput.value = '';
            userGuessInput.focus();
        }

        // اتصال رویدادها به دکمه‌ها و اینپوت
        guessBtn.addEventListener('click', handleGuess);
        userGuessInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                handleGuess();
            }
        });
        restartBtn.addEventListener('click', resetGame);

        // تمرکز اولیه روی فیلد ورودی زمان بارگذاری صفحه
        userGuessInput.focus();
    </script>
</body>
</html>

بررسی بخش‌به‌بخش کد و منطق برنامه‌نویسی آن

این اسکریپت ساده بر پایه‌ی اصول استاندارد جاوااسکریپت نوشته شده است. در ادامه بخش‌های کلیدی منطق پیاده‌سازی شده را مرور می‌کنیم:

  • تولید عدد تصادفی: با استفاده از ترکیب Math.random() و Math.floor() یک عدد صحیح تصادفی در بازه ۱ تا ۱۰۰ تولید می‌شود که تا زمان برد کاربر تغییر نمی‌کند.
  • اعتبارسنجی ورودی: تابع isNaN همراه با بررسی محدوده‌ها تضمین می‌کند که اگر کاربر رشته‌های خالی، حروف یا اعداد خارج از محدوده وارد کرد، سیستم دچار خطا نشده و با نمایش هشدار به او اجازه اصلاح دهد.
  • مدیریت ذخیره‌سازی محلی (localStorage): متد localStorage.getItem و localStorage.setItem برای این به کار رفته‌اند که کمترین تعداد تلاش کاربر به عنوان رکورد طلایی ذخیره شود و حتی با رفرش صفحه از بین نرود.
  • مدیریت رویدادهای صفحه کلید: علاوه بر کلیک روی دکمه «بررسی»، با شنیدن رویداد keypress و کلید Enter، کاربر می‌تواند با سرعت بیشتری اعداد خود را ارسال کند.

اشتباهات رایج و نکات پنهان در توسعه این پروژه

هنگام کار بر روی پروژه‌های مشابه، برنامه‌نویسان تازه‌کار معمولاً با چند خطای پرتکرار مواجه می‌شوند که دانستن آن‌ها خالی از لطف نیست:

  1. مقایسه رشته با عدد: مقادیر دریافتی از تگ‌های <input> همگی به شکل رشته (String) هستند. اگر از تبدیل صریح با parseInt() استفاده نکنید، ممکن است مقایسه‌های منطقی دچار اختلال شوند.
  2. فراموش کردن پاکسازی حالت‌ها: هنگام شروع مجدد بازی (Reset)، حتماً باید متغیرهایی نظیر شمارنده تلاش‌ها و عدد مخفی مقداردهی اولیه شوند؛ در غیر این صورت بازی در حالت قبلی فریز می‌ماند.

پرسش‌های متداول

چگونه می‌توانم محدوده اعداد بازی را از ۱ تا ۱۰۰ به اعداد بزرگتری تغییر دهم؟

برای این کار کافی است در بخش تولید عدد تصادفی، مقدار ۱۰۰ را به عدد دلخواه خود (مثلاً ۱۰۰۰) تغییر دهید و همین تغییر را در ویژگی‌های max تگ اینپوت HTML و متون راهنمای صفحه اعمال کنید.

چرا رکورد ذخیره شده پس از بستن کامل مرورگر پاک نمی‌شود؟

به این دلیل که ما از رابط برنامه‌نویسی localStorage استفاده کرده‌ایم. این ویژگی مرورگر، داده‌ها را بدون تاریخ انقضا ذخیره می‌کند و حتی با بسته شدن تب یا مرورگر نیز داده‌ها حفظ خواهند شد.

آیا می‌توانم این کد را روی موبایل یا تبلت اجرا کنم؟

بله! طراحی این صفحه کاملاً واکنش‌گرا (Responsive) انجام شده و استایل‌های CSS آن با اندازه‌های مختلف صفحه نمایش سازگار هستند.

جمع‌بندی

در این مقاله یک پروژه‌ی کامل، تعاملی و زیبا برای ساخت بازی حدس عدد با جاوااسکریپت خالص را پیاده‌سازی کردیم. شما یاد گرفتید که چگونه ورودی‌ها را اعتبارسنجی کنید، انیمیشن‌های CSS سفارشی بسازید و از حافظه محلی مرورگر برای ثبت رکورد استفاده کنید. این دست پروژه‌ها سکوی پرتاب مناسبی برای درک عمیق‌تر مفاهیم فرانت‌اند هستند و می‌توانید با افزودن قابلیت‌هایی مثل محدودیت تعداد حدس یا زمان‌سنج، آن را توسعه دهید.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *