这会是一个详细的、分步骤的教程,涵盖了从 HTML 结构到 CSS 样式,再到核心的 JavaScript 游戏逻辑,我们将按照《JavaScript高级程序设计》(通常被称为“红宝书”)第12章的风格,以面向对象和模块化的方式来组织代码,使其清晰、易于理解和扩展。

js第12章网页版2048小游戏
(图片来源网络,侵删)

第一步:游戏整体规划

在开始写代码之前,我们先明确一下游戏的核心要素和实现思路。

  1. 游戏状态

    • 一个 4x4 的网格,用于存放数字方块。
    • 当前分数。
    • 游戏是否结束。
  2. 游戏逻辑

    • 初始化:在 4x4 的网格中随机生成两个数字(2 或 4)。
    • 移动:响应键盘事件(上下左右),将所有方块朝指定方向移动。
    • 合并:在移动过程中,如果两个相同数字的方块相撞,它们会合并成一个,并且分数加倍。
    • 填充:每次移动后,在空白位置随机生成一个新的数字(2 或 4)。
    • 判断:判断游戏是否结束(没有空白位置且相邻方块都无法合并)。
    • 胜利:判断是否达到 2048(可以继续玩,但算胜利)。
  3. 技术实现

    js第12章网页版2048小游戏
    (图片来源网络,侵删)
    • HTML:创建游戏容器、分数显示、网格和方块。
    • CSS:美化游戏界面,设计不同数字方块的颜色和样式,并实现平滑的移动动画。
    • JavaScript
      • 使用一个类 Game2048 来封装所有的游戏逻辑。
      • 用一个二维数组 grid 来表示游戏状态。
      • 使用事件监听来捕获用户的键盘输入。

第二步: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">2048 游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>2048</h1>
        <div class="game-info">
            <div class="score-container">
                <span>分数:</span>
                <span id="score">0</span>
            </div>
            <button id="new-game-btn">新游戏</button>
        </div>
        <div class="game-container">
            <div class="game-message">
                <p></p>
                <div class="lower">
                    <button id="retry-button">再试一次</button>
                </div>
            </div>
            <div class="grid-container">
                <!-- 4x4 网格背景 -->
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
            </div>
            <!-- 用于动态添加数字方块的容器 -->
            <div class="tile-container" id="tile-container"></div>
        </div>
        <p class="game-explanation">
            <strong>游戏玩法:</strong> 使用 <strong>方向键</strong> 移动方块,当两个相同数字的方块碰到一起时,它们会 <strong>合并成一个</strong>!
        </p>
    </div>
    <script src="game.js"></script>
</body>
</html>

第三步:CSS 样式

我们为游戏添加漂亮的样式,文件名为 style.css,这里我们使用了 CSS Grid 和 Flexbox 来布局,并定义了不同数字方块的颜色。

/* style.css */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}
body {
    font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
    background-color: #faf8ef;
    color: #776e65;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}
.container {
    text-align: center;
}
h1 {
    font-size: 80px;
    font-weight: bold;
    margin: 20px 0;
}
.game-info {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 20px;
    margin-bottom: 20px;
}
.score-container {
    background-color: #bbada0;
    padding: 10px 20px;
    border-radius: 3px;
    color: white;
    font-size: 25px;
}
#new-game-btn {
    background-color: #8f7a66;
    color: #f9f6f2;
    border: none;
    border-radius: 3px;
    padding: 10px 20px;
    font-size: 18px;
    cursor: pointer;
    transition: background-color 0.3s;
}
#new-game-btn:hover {
    background-color: #9f8a76;
}
.game-container {
    position: relative;
    width: 500px;
    height: 500px;
    background-color: #bbada0;
    border-radius: 6px;
    padding: 10px;
    margin: 0 auto;
}
.grid-container {
    position: absolute;
    z-index: 1;
}
.grid-row {
    display: flex;
    margin-bottom: 10px;
}
.grid-row:last-child {
    margin-bottom: 0;
}
.grid-cell {
    width: 107.5px;
    height: 107.5px;
    background-color: rgba(238, 228, 218, 0.35);
    border-radius: 3px;
    margin-right: 10px;
}
.grid-cell:last-child {
    margin-right: 0;
}
.tile-container {
    position: absolute;
    z-index: 2;
    /* 关键:让内部的方块可以绝对定位 */
    width: 100%;
    height: 100%;
}
.tile {
    position: absolute;
    width: 107.5px;
    height: 107.5px;
    background-color: #eee4da;
    border-radius: 3px;
    font-size: 55px;
    font-weight: bold;
    display: flex;
    justify-content: center;
    align-items: center;
    transition: all 0.15s ease-in-out;
    line-height: 1;
}
/* 不同数字的方块颜色 */
.tile-2 { background-color: #eee4da; color: #776e65; }
.tile-4 { background-color: #ede0c8; color: #776e65; }
.tile-8 { background-color: #f2b179; color: #f9f6f2; }
.tile-16 { background-color: #f59563; color: #f9f6f2; }
.tile-32 { background-color: #f67c5f; color: #f9f6f2; }
.tile-64 { background-color: #f65e3b; color: #f9f6f2; }
.tile-128 { background-color: #edcf72; color: #f9f6f2; font-size: 45px; }
.tile-256 { background-color: #edcc61; color: #f9f6f2; font-size: 45px; }
.tile-512 { background-color: #edc850; color: #f9f6f2; font-size: 45px; }
.tile-1024 { background-color: #edc53f; color: #f9f6f2; font-size: 35px; }
.tile-2048 { background-color: #edc22e; color: #f9f6f2; font-size: 35px; }
/* 游戏结束或胜利的遮罩层 */
.game-message {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, 0.73);
    z-index: 100;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    border-radius: 6px;
}
.game-message.game-won {
    display: flex;
    background-color: rgba(237, 194, 46, 0.5);
}
.game-message.game-over {
    display: flex;
    background-color: rgba(238, 228, 218, 0.73);
}
.game-message p {
    font-size: 60px;
    font-weight: bold;
    margin-bottom: 20px;
}
#retry-button {
    background-color: #8f7a66;
    color: #f9f6f2;
    border: none;
    border-radius: 3px;
    padding: 10px 20px;
    font-size: 18px;
    cursor: pointer;
}

第四步:JavaScript 游戏逻辑

这是最核心的部分,我们将所有逻辑封装在一个 Game2048 类中,文件名为 game.js

// game.js
class Game2048 {
    constructor() {
        // 游戏状态
        this.grid = [];
        this.score = 0;
        this.size = 4; // 4x4 网格
        // DOM 元素
        this.tileContainer = document.getElementById('tile-container');
        this.scoreDisplay = document.getElementById('score');
        this.messageDisplay = document.querySelector('.game-message p');
        this.gameContainer = document.querySelector('.game-container');
        this.newGameButton = document.getElementById('new-game-btn');
        this.retryButton = document.getElementById('retry-button');
        // 初始化
        this.init();
    }
    init() {
        // 绑定事件
        this.setupEventListeners();
        // 开始新游戏
        this.newGame();
    }
    setupEventListeners() {
        // 键盘事件
        document.addEventListener('keydown', (event) => {
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
                event.preventDefault(); // 防止页面滚动
                this.move(event.key);
            }
        });
        // 新游戏按钮
        this.newGameButton.addEventListener('click', () => this.newGame());
        this.retryButton.addEventListener('click', () => this.newGame());
    }
    newGame() {
        // 重置游戏状态
        this.score = 0;
        this.updateScore();
        this.clearMessage();
        this.clearTiles();
        // 初始化空网格
        this.grid = Array(this.size).fill().map(() => Array(this.size).fill(0));
        // 添加两个初始方块
        this.addNewTile();
        this.addNewTile();
        // 更新视图
        this.updateView();
    }
    // 在空白位置随机添加一个新方块 (90%概率是2, 10%概率是4)
    addNewTile() {
        const emptyCells = [];
        for (let r = 0; r < this.size; r++) {
            for (let c = 0; c < this.size; c++) {
                if (this.grid[r][c] === 0) {
                    emptyCells.push({ r, c });
                }
            }
        }
        if (emptyCells.length > 0) {
            const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
            this.grid[randomCell.r][randomCell.c] = Math.random() < 0.9 ? 2 : 4;
        }
    }
    // 核心移动逻辑
    move(direction) {
        let moved = false;
        const previousGrid = this.grid.map(row => [...row]); // 深拷贝,用于比较是否真的发生了移动
        if (direction === 'ArrowLeft') {
            for (let r = 0; r < this.size; r++) {
                const row = this.slideAndMergeRow(this.grid[r]);
                if (JSON.stringify(row) !== JSON.stringify(this.grid[r])) {
                    moved = true;
                }
                this.grid[r] = row;
            }
        } else if (direction === 'ArrowRight') {
            for (let r = 0; r < this.size; r++) {
                const row = this.slideAndMergeRow(this.grid[r].slice().reverse()).reverse();
                if (JSON.stringify(row) !== JSON.stringify(this.grid[r])) {
                    moved = true;
                }
                this.grid[r] = row;
            }
        } else if (direction === 'ArrowUp') {
            for (let c = 0; c < this.size; c++) {
                const column = [];
                for (let r = 0; r < this.size; r++) {
                    column.push(this.grid[r][c]);
                }
                const newColumn = this.slideAndMergeRow(column);
                if (JSON.stringify(newColumn) !== JSON.stringify(column)) {
                    moved = true;
                }
                for (let r = 0; r < this.size; r++) {
                    this.grid[r][c] = newColumn[r];
                }
            }
        } else if (direction === 'ArrowDown') {
            for (let c = 0; c < this.size; c++) {
                const column = [];
                for (let r = 0; r < this.size; r++) {
                    column.push(this.grid[r][c]);
                }
                const newColumn = this.slideAndMergeRow(column.slice().reverse()).reverse();
                if (JSON.stringify(newColumn) !== JSON.stringify(column)) {
                    moved = true;
                }
                for (let r = 0; r < this.size; r++) {
                    this.grid[r][c] = newColumn[r];
                }
            }
        }
        if (moved) {
            this.addNewTile();
            this.updateView();
            this.updateScore();
            if (this.checkWin()) {
                this.showMessage('你赢了!');
            } else if (this.checkGameOver()) {
                this.showMessage('游戏结束!');
            }
        }
    }
    // 处理单行/列的滑动和合并
    slideAndMergeRow(line) {
        // 移除零
        let newLine = line.filter(val => val !== 0);
        // 合并相同的数字
        for (let i = 0; i < newLine.length - 1; i++) {
            if (newLine[i] === newLine[i + 1]) {
                newLine[i] *= 2;
                this.score += newLine[i];
                newLine.splice(i + 1, 1);
            }
        }
        // 用零填充到行尾
        while (newLine.length < this.size) {
            newLine.push(0);
        }
        return newLine;
    }
    // 更新视图
    updateView() {
        this.clearTiles();
        for (let r = 0; r < this.size; r++) {
            for (let c = 0; c < this.size; c++) {
                if (this.grid[r][c] !== 0) {
                    this.createTile(r, c, this.grid[r][c]);
                }
            }
        }
    }
    // 创建一个方块元素
    createTile(row, col, value) {
        const tile = document.createElement('div');
        tile.className = `tile tile-${value}`;
        tile.textContent = value;
        // 计算位置
        const posX = col * (107.5 + 10); // 宽度 + 间距
        const posY = row * (107.5 + 10);
        tile.style.transform = `translate(${posX}px, ${posY}px)`;
        this.tileContainer.appendChild(tile);
    }
    // 清除所有动态生成的方块
    clearTiles() {
        this.tileContainer.innerHTML = '';
    }
    // 更新分数显示
    updateScore() {
        this.scoreDisplay.textContent = this.score;
    }
    // 检查是否获胜 (达到2048)
    checkWin() {
        for (let r = 0; r < this.size; r++) {
            for (let c = 0; c < this.size; c++) {
                if (this.grid[r][c] === 2048) {
                    return true;
                }
            }
        }
        return false;
    }
    // 检查游戏是否结束
    checkGameOver() {
        // 1. 检查是否还有空格
        for (let r = 0; r < this.size; r++) {
            for (let c = 0; c < this.size; c++) {
                if (this.grid[r][c] === 0) {
                    return false;
                }
            }
        }
        // 2. 检查是否有相邻的相同数字
        for (let r = 0; r < this.size; r++) {
            for (let c = 0; c < this.size; c++) {
                const current = this.grid[r][c];
                // 检查右边
                if (c < this.size - 1 && current === this.grid[r][c + 1]) {
                    return false;
                }
                // 检查下边
                if (r < this.size - 1 && current === this.grid[r + 1][c]) {
                    return false;
                }
            }
        }
        return true; // 无法移动,游戏结束
    }
    // 显示游戏结束或胜利信息
    showMessage(text) {
        this.messageDisplay.textContent = text;
        this.gameContainer.classList.add('game-won', 'game-over');
        if (text === '你赢了!') {
            this.gameContainer.classList.remove('game-over');
        } else {
            this.gameContainer.classList.remove('game-won');
        }
    }
    // 清除消息
    clearMessage() {
        this.gameContainer.classList.remove('game-won', 'game-over');
    }
}
// 当页面加载完成后,启动游戏
document.addEventListener('DOMContentLoaded', () => {
    new Game2048();
});

总结与扩展

将这三个文件(index.html, style.css, game.js)放在同一个文件夹下,用浏览器打开 index.html,你就可以开始玩 2048 游戏了!

代码结构回顾(对标红宝书第12章):

  • 面向对象:我们使用 class Game2048 将所有相关的数据(grid, score)和方法(move, addNewTile 等)封装在一起,避免了全局变量,使代码结构更清晰,易于维护。
  • 事件处理:我们使用 addEventListener 来监听键盘和按钮事件,这是现代 JavaScript 中处理用户交互的标准方式。
  • DOM 操作:代码中包含了动态创建、修改和删除 DOM 元素的操作,以实现游戏界面的实时更新。
  • 数据与视图分离:游戏的核心数据存储在 this.gridthis.score 中。updateView() 方法负责将数据的变化反映到页面上,这种分离使得逻辑和表现层解耦。

可以进一步扩展的功能

  1. 本地最高分:使用 localStorage 来保存和读取历史最高分。
  2. 动画优化:使用 CSS transformtransition 实现更流畅的方块移动和合并动画,当前版本在添加新方块时有动画,但移动时是瞬间完成的,要实现移动动画,需要记录每个方块移动的起始和结束位置,并应用更复杂的 CSS。
  3. 触摸支持:为移动设备添加触摸滑动事件,支持手指操作。
  4. 撤销功能:增加一个“撤销”按钮,允许玩家回退到上一步。

这个实现为你提供了一个功能完整、结构清晰的 2048 游戏代码,希望对你有帮助!