Day-21-项目3-猜数字游戏

Day 21: 项目3-猜数字游戏

🎯 Day 21 – 猜数字游戏
📂 分类: 09-项目实战
🔥 难度: ⭐⭐⭐☆☆


🎯 学习目标

  • ✅ 掌握 Math.random() 和随机数生成
  • ✅ 学会表单验证和用户输入处理
  • ✅ 理解游戏循环和状态管理
  • ✅ 掌握事件监听和交互逻辑
  • ✅ 学会使用 CSS 动画增强体验

💡 项目概述

项目简介

猜数字游戏是学习交互式编程的经典项目,它包含:

  • 🎲 随机数生成:Math.random() 的实际应用
  • 🎮 游戏循环:开始 → 猜测 → 反馈 → 继续
  • 🏆 胜负判定:比较猜测与目标数字
  • 📊 统计追踪:记录猜测次数和最佳成绩
  • 🎨 UI 反馈:视觉提示和动画效果

核心功能

  • [ ] 生成 1-100 的随机数
  • [ ] 接受用户输入并验证
  • [ ] 判断大小并给出提示
  • [ ] 记录猜测次数
  • [ ] 重新开始游戏
  • [ ] 历史最佳成绩

技术重点

  • 随机数: Math.random()、Math.floor()
  • DOM 操作: 表单、按钮、文本
  • 事件处理: submit、click、keypress
  • 状态管理: 游戏状态、历史记录
  • CSS 动画: 过渡效果、视觉反馈

📝 项目结构

guess-game/
├── index.html          # 主页面
├── style.css           # 样式文件
└── script.js           # JavaScript逻辑

🎮 完整代码

1. HTML (index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>猜数字游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>🎲 猜数字游戏</h1>
            <p class="subtitle">我有一个 1-100 之间的数字,你能猜到吗?</p>
        </header>

        <!-- 游戏信息 -->
        <div class="game-info">
            <div class="info-item">
                <span class="label">尝试次数</span>
                <span class="value" id="guessCount">0</span>
            </div>
            <div class="info-item">
                <span class="label">历史最佳</span>
                <span class="value" id="bestScore">-</span>
            </div>
        </div>

        <!-- 游戏区域 -->
        <div class="game-area">
            <!-- 输入区域 -->
            <div class="input-section">
                <label for="guessInput">输入你的猜测 (1-100)</label>
                <div class="input-group">
                    <input 
                        type="number" 
                        id="guessInput" 
                        min="1" 
                        max="100" 
                        placeholder="输入数字..."
                        autocomplete="off"
                    >
                    <button id="submitBtn" class="btn btn-primary">猜!</button>
                </div>
                <p class="hint" id="inputHint"></p>
            </div>

            <!-- 反馈区域 -->
            <div class="feedback-section" id="feedback">
                <div class="feedback-icon" id="feedbackIcon">❓</div>
                <div class="feedback-message" id="feedbackMessage">
                    开始游戏吧!输入你的猜测
                </div>
                <div class="feedback-detail" id="feedbackDetail"></div>
            </div>

            <!-- 历史记录 -->
            <div class="history-section">
                <h3>猜测历史</h3>
                <div class="history-list" id="historyList">
                    <p class="empty">还没有猜测记录</p>
                </div>
            </div>

            <!-- 重新开始 -->
            <button id="restartBtn" class="btn btn-secondary" style="display: none;">
                🔄 重新开始
            </button>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

2. CSS (style.css)

/* 全局样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
}

/* 容器 */
.container {
    background: white;
    border-radius: 20px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    max-width: 500px;
    width: 100%;
    padding: 30px;
    animation: fadeIn 0.5s ease;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* 头部 */
header {
    text-align: center;
    margin-bottom: 30px;
}

header h1 {
    color: #333;
    font-size: 32px;
    margin-bottom: 10px;
}

.subtitle {
    color: #666;
    font-size: 14px;
}

/* 游戏信息 */
.game-info {
    display: flex;
    justify-content: space-around;
    background: #f7f7f7;
    border-radius: 12px;
    padding: 15px;
    margin-bottom: 30px;
}

.info-item {
    text-align: center;
}

.info-item .label {
    display: block;
    color: #666;
    font-size: 12px;
    margin-bottom: 5px;
}

.info-item .value {
    display: block;
    color: #667eea;
    font-size: 24px;
    font-weight: bold;
}

/* 游戏区域 */
.game-area {
    margin-top: 20px;
}

/* 输入区域 */
.input-section {
    margin-bottom: 20px;
}

.input-section label {
    display: block;
    color: #333;
    font-weight: 600;
    margin-bottom: 10px;
}

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

#guessInput {
    flex: 1;
    padding: 15px;
    border: 2px solid #e0e0e0;
    border-radius: 10px;
    font-size: 18px;
    text-align: center;
    transition: border-color 0.3s;
}

#guessInput:focus {
    outline: none;
    border-color: #667eea;
}

.btn {
    padding: 15px 30px;
    border: none;
    border-radius: 10px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;
}

.btn-primary {
    background: #667eea;
    color: white;
}

.btn-primary:hover {
    background: #5568d3;
    transform: scale(1.05);
}

.btn-secondary {
    background: #f7f7f7;
    color: #333;
    width: 100%;
    margin-top: 20px;
}

.btn-secondary:hover {
    background: #e0e0e0;
}

.hint {
    color: #999;
    font-size: 12px;
    margin-top: 8px;
    min-height: 18px;
}

.hint.error {
    color: #f56565;
}

.hint.success {
    color: #48bb78;
}

/* 反馈区域 */
.feedback-section {
    background: #f7f7f7;
    border-radius: 12px;
    padding: 20px;
    text-align: center;
    margin-bottom: 20px;
    transition: all 0.3s;
}

.feedback-section.too-high {
    background: #fed7d7;
    border: 2px solid #fc8181;
}

.feedback-section.too-low {
    background: #bee3f8;
    border: 2px solid #63b3ed;
}

.feedback-section.correct {
    background: #c6f6d5;
    border: 2px solid #48bb78;
    animation: celebrate 0.5s ease;
}

@keyframes celebrate {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.05); }
}

.feedback-icon {
    font-size: 48px;
    margin-bottom: 10px;
}

.feedback-message {
    font-size: 20px;
    font-weight: bold;
    color: #333;
    margin-bottom: 10px;
}

.feedback-detail {
    font-size: 14px;
    color: #666;
}

/* 历史记录 */
.history-section {
    margin-bottom: 20px;
}

.history-section h3 {
    font-size: 16px;
    color: #333;
    margin-bottom: 10px;
}

.history-list {
    background: #f7f7f7;
    border-radius: 10px;
    padding: 15px;
    max-height: 200px;
    overflow-y: auto;
}

.history-list .empty {
    color: #999;
    text-align: center;
    font-size: 14px;
}

.history-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px;
    border-bottom: 1px solid #e0e0e0;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateX(-10px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

.history-item:last-child {
    border-bottom: none;
}

.history-number {
    font-weight: bold;
    color: #333;
}

.history-arrow {
    font-size: 12px;
    margin: 0 10px;
}

.history-arrow.high {
    color: #fc8181;
}

.history-arrow.low {
    color: #63b3ed;
}

/* 响应式 */
@media (max-width: 480px) {
    .container {
        padding: 20px;
    }
    
    header h1 {
        font-size: 24px;
    }
    
    .input-group {
        flex-direction: column;
    }
    
    .btn-primary {
        width: 100%;
    }
}

3. JavaScript (script.js)

// ========== 游戏状态 ==========
class GuessGame {
    constructor() {
        this.targetNumber = this.generateRandomNumber();
        this.guessCount = 0;
        this.guessHistory = [];
        this.gameOver = false;
        this.bestScore = this.loadBestScore();
        
        this.initializeElements();
        this.attachEventListeners();
        this.updateDisplay();
        this.loadBestScore();
    }

    // 初始化 DOM 元素
    initializeElements() {
        this.elements = {
            guessInput: document.getElementById('guessInput'),
            submitBtn: document.getElementById('submitBtn'),
            restartBtn: document.getElementById('restartBtn'),
            guessCount: document.getElementById('guessCount'),
            bestScore: document.getElementById('bestScore'),
            feedback: document.getElementById('feedback'),
            feedbackIcon: document.getElementById('feedbackIcon'),
            feedbackMessage: document.getElementById('feedbackMessage'),
            feedbackDetail: document.getElementById('feedbackDetail'),
            historyList: document.getElementById('historyList'),
            inputHint: document.getElementById('inputHint')
        };
    }

    // 附加事件监听器
    attachEventListeners() {
        // 提交按钮
        this.elements.submitBtn.addEventListener('click', () => {
            this.handleGuess();
        });

        // 回车键提交
        this.elements.guessInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                this.handleGuess();
            }
        });

        // 输入验证
        this.elements.guessInput.addEventListener('input', () => {
            this.validateInput();
        });

        // 重新开始
        this.elements.restartBtn.addEventListener('click', () => {
            this.restart();
        });
    }

    // 生成随机数 (1-100)
    generateRandomNumber() {
        return Math.floor(Math.random() * 100) + 1;
    }

    // 验证输入
    validateInput() {
        const value = this.elements.guessInput.value;
        const hint = this.elements.inputHint;
        
        if (value === '') {
            hint.textContent = '';
            hint.className = 'hint';
            return false;
        }

        const num = parseInt(value);
        
        if (isNaN(num)) {
            hint.textContent = '请输入有效的数字';
            hint.className = 'hint error';
            return false;
        }

        if (num < 1 || num > 100) {
            hint.textContent = '数字必须在 1-100 之间';
            hint.className = 'hint error';
            return false;
        }

        hint.textContent = '✓ 有效';
        hint.className = 'hint success';
        return true;
    }

    // 处理猜测
    handleGuess() {
        if (this.gameOver) {
            return;
        }

        if (!this.validateInput()) {
            this.shakeInput();
            return;
        }

        const guess = parseInt(this.elements.guessInput.value);
        this.guessCount++;
        
        // 添加到历史
        this.guessHistory.push(guess);
        
        // 判断结果
        if (guess === this.targetNumber) {
            this.handleCorrectGuess(guess);
        } else if (guess > this.targetNumber) {
            this.handleTooHigh(guess);
        } else {
            this.handleTooLow(guess);
        }
        
        // 更新显示
        this.updateDisplay();
        this.addToHistory(guess, guess > this.targetNumber ? 'high' : 'low');
        
        // 清空输入并聚焦
        if (!this.gameOver) {
            this.elements.guessInput.value = '';
            this.elements.guessInput.focus();
        }
    }

    // 猜对了
    handleCorrectGuess(guess) {
        this.gameOver = true;
        
        // 更新最佳成绩
        if (this.bestScore === null || this.guessCount < this.bestScore) {
            this.bestScore = this.guessCount;
            this.saveBestScore();
        }
        
        // 显示成功反馈
        this.elements.feedback.className = 'feedback-section correct';
        this.elements.feedbackIcon.textContent = '🎉';
        this.elements.feedbackMessage.textContent = '恭喜你,猜对了!';
        this.elements.feedbackDetail.textContent = 
            `数字是 ${this.targetNumber},你用了 ${this.guessCount} 次猜测`;
        
        // 显示重新开始按钮
        this.elements.restartBtn.style.display = 'block';
        this.elements.submitBtn.disabled = true;
        this.elements.guessInput.disabled = true;
    }

    // 猜大了
    handleTooHigh(guess) {
        this.elements.feedback.className = 'feedback-section too-high';
        this.elements.feedbackIcon.textContent = '📈';
        this.elements.feedbackMessage.textContent = '太大了!';
        this.elements.feedbackDetail.textContent = 
            `你的猜测 ${guess} 比目标数字大`;
    }

    // 猜小了
    handleTooLow(guess) {
        this.elements.feedback.className = 'feedback-section too-low';
        this.elements.feedbackIcon.textContent = '📉';
        this.elements.feedbackMessage.textContent = '太小了!';
        this.elements.feedbackDetail.textContent = 
            `你的猜测 ${guess} 比目标数字小`;
    }

    // 添加到历史记录
    addToHistory(guess, direction) {
        const historyItem = document.createElement('div');
        historyItem.className = 'history-item';
        
        const arrow = direction === 'high' ? '↓' : '↑';
        const arrowClass = direction === 'high' ? 'high' : 'low';
        
        historyItem.innerHTML = `
            <span class="history-number">#${this.guessCount}: ${guess}</span>
            <span class="history-arrow ${arrowClass}">${arrow}</span>
        `;
        
        // 移除空提示
        const empty = this.elements.historyList.querySelector('.empty');
        if (empty) {
            empty.remove();
        }
        
        // 添加到开头
        this.elements.historyList.insertBefore(
            historyItem,
            this.elements.historyList.firstChild
        );
    }

    // 更新显示
    updateDisplay() {
        this.elements.guessCount.textContent = this.guessCount;
        this.elements.bestScore.textContent = 
            this.bestScore !== null ? this.bestScore : '-';
    }

    // 输入框抖动效果
    shakeInput() {
        const input = this.elements.guessInput;
        input.style.animation = 'none';
        input.offsetHeight;  // 触发重绘
        input.style.animation = 'shake 0.5s ease';
    }

    // 重新开始
    restart() {
        this.targetNumber = this.generateRandomNumber();
        this.guessCount = 0;
        this.guessHistory = [];
        this.gameOver = false;
        
        // 重置 UI
        this.elements.feedback.className = 'feedback-section';
        this.elements.feedbackIcon.textContent = '❓';
        this.elements.feedbackMessage.textContent = '开始游戏吧!输入你的猜测';
        this.elements.feedbackDetail.textContent = '';
        
        this.elements.restartBtn.style.display = 'none';
        this.elements.submitBtn.disabled = false;
        this.elements.guessInput.disabled = false;
        this.elements.guessInput.value = '';
        this.elements.guessInput.focus();
        
        this.elements.historyList.innerHTML = '<p class="empty">还没有猜测记录</p>';
        
        this.updateDisplay();
    }

    // 加载最佳成绩
    loadBestScore() {
        const saved = localStorage.getItem('guessGameBestScore');
        if (saved) {
            this.bestScore = parseInt(saved);
        }
    }

    // 保存最佳成绩
    saveBestScore() {
        localStorage.setItem('guessGameBestScore', this.bestScore);
    }
}

// 添加抖动动画
const style = document.createElement('style');
style.textContent = `
    @keyframes shake {
        0%, 100% { transform: translateX(0); }
        10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
        20%, 40%, 60%, 80% { transform: translateX(5px); }
    }
`;
document.head.appendChild(style);

// 初始化游戏
document.addEventListener('DOMContentLoaded', () => {
    const game = new GuessGame();
    console.log('🎮 猜数字游戏已启动!');
    console.log('💡 提示:按回车键快速提交猜测');
});

🎯 项目亮点

1. 完整的游戏循环

开始 → 生成随机数 → 接受输入 → 判断 → 反馈 → 继续/结束

2. 状态管理

class GuessGame {
    constructor() {
        this.targetNumber = null;      // 目标数字
        this.guessCount = 0;           // 猜测次数
        this.guessHistory = [];        // 历史记录
        this.gameOver = false;         // 游戏状态
        this.bestScore = null;         // 最佳成绩
    }
}

3. 用户输入验证

validateInput() {
    // 检查是否为数字
    // 检查范围是否有效
    // 提供实时反馈
}

4. 本地存储最佳成绩

// 保存到 localStorage
localStorage.setItem('guessGameBestScore', this.bestScore);

// 从 localStorage 加载
const saved = localStorage.getItem('guessGameBestScore');

✍️ 练习任务

练习1: 基础功能(30分钟)

  • [ ] 实现随机数生成 (1-100)
  • [ ] 实现输入验证
  • [ ] 实现大小比较逻辑
  • [ ] 显示正确/错误反馈
  • [ ] 测试基本功能

练习2: 进阶功能(45分钟)

  • [ ] 添加猜测次数统计
  • [ ] 实现历史记录显示
  • [ ] 添加最佳成绩保存
  • [ ] 实现重新开始功能
  • [ ] 添加输入动画效果

练习3: UI 优化(30分钟)

  • [ ] 添加欢迎界面
  • [ ] 实现猜测动画
  • [ ] 优化移动端布局
  • [ ] 添加音效提示
  • [ ] 美化反馈样式

练习4: 功能扩展(60分钟)

  • [ ] 添加难度选择(简单/困难)
  • [ ] 实现计时器功能
  • [ ] 添加提示功能(显示范围)
  • [ ] 实现多人模式
  • [ ] 添加成就系统

💡 常见问题 FAQ

Q1: Math.random() 真的随机吗?

A: 不是真随机,是伪随机。但对于游戏来说足够了。

// 生成 1-100 的随机整数
Math.floor(Math.random() * 100) + 1;

Q2: 如何防止用户输入无效数字?

A: 使用 HTML5 验证 + JavaScript 验证。

<input type="number" min="1" max="100">
if (num < 1 || num > 100) {
    // 显示错误
}

Q3: 如何让游戏更有趣?

A: 可以添加:

  • 倒计时模式
  • 有限次猜测
  • 提示功能
  • 难度级别

Q4: 最佳成绩如何持久化?

A: 使用 localStorage。

// 保存
localStorage.setItem('bestScore', score);

// 读取
const score = localStorage.getItem('bestScore');

📚 拓展学习

推荐项目

  • 石头剪刀布:理解概率和选择
  • 井字棋:学习 AI 和游戏逻辑
  • 记忆翻牌:掌握状态管理

相关主题

  • 数组方法(push、filter、map)
  • 对象和类
  • DOM 操作
  • 事件处理

🎓 总结

今天我们学到了

  • ✅ Math.random() 生成随机数
  • ✅ 表单验证和用户输入处理
  • ✅ 游戏状态管理
  • ✅ localStorage 数据持久化
  • ✅ 完整的游戏开发流程

关键概念

  • 🎲 随机数:Math.random() + Math.floor()
  • 🎮 游戏循环:输入 → 处理 → 反馈
  • 💾 状态管理:跟踪游戏数据
  • 🎨 UI 反馈:视觉和动画效果

学习时间: 2026-03-17
难度: ⭐⭐⭐☆☆
预计用时: 3-4 小时
关键词: 猜数字、随机数、游戏开发、DOM操作
相关标签: #09-项目实战


📌 下一步: 学习 Day 22: 项目4-打字练习游戏,继续游戏开发之旅!