1. 项目结构与 HTML
  2. CSS 样式设计
  3. JavaScript 游戏逻辑
    • 初始化游戏
    • 渲染游戏盘面
    • 移动和合并逻辑
    • 键盘事件监听
    • 游戏状态判断(胜利/失败)
    • 分数计算

项目结构与 HTML

我们创建三个文件:index.html, style.css, 和 script.js

js开发2048网页小游戏
(图片来源网络,侵删)

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">
        <header>
            <h1>2048</h1>
            <div class="score-container">
                <div class="score-box">
                    <span class="score-label">分数</span>
                    <span id="score">0</span>
                </div>
                <div class="score-box">
                    <span class="score-label">最高分</span>
                    <span id="best-score">0</span>
                </div>
            </div>
        </header>
        <main>
            <div class="game-container">
                <div class="game-message">
                    <p></p>
                    <div class="lower">
                        <a class="keep-playing-button">继续游戏</a>
                        <a class="retry-button">再玩一次</a>
                    </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> 移动方块,当两个相同数字的方块碰撞时,它们会合并成一个!
            </p>
            <button class="new-game-button" id="new-game-button">新游戏</button>
        </main>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS 样式设计

style.css 这部分负责美化游戏界面,使其看起来像真正的 2048。

/* 基础样式和变量 */
:root {
    --grid-background-color: #bbada0;
    --grid-cell-size: 100px;
    --grid-cell-gap: 10px;
    --tile-color-2: #eee4da;
    --tile-color-4: #ede0c8;
    --tile-color-8: #f2b179;
    --tile-color-16: #f59563;
    --tile-color-32: #f67c5f;
    --tile-color-64: #f65e3b;
    --tile-color-128: #edcf72;
    --tile-color-256: #edcc61;
    --tile-color-512: #edc850;
    --tile-color-1024: #edc53f;
    --tile-color-2048: #edc22e;
}
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;
    margin: 0;
}
.container {
    text-align: center;
    max-width: 500px;
    width: 100%;
    padding: 20px;
}
header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}
h1 {
    font-size: 80px;
    font-weight: bold;
    margin: 0;
    color: #776e65;
}
.score-container {
    display: flex;
    gap: 10px;
}
.score-box {
    background-color: #bbada0;
    padding: 10px 20px;
    border-radius: 3px;
    color: white;
    position: relative;
}
.score-label {
    font-size: 13px;
    text-transform: uppercase;
}
#score, #best-score {
    font-size: 25px;
    font-weight: bold;
}
/* 游戏区域 */
.game-container {
    position: relative;
    background-color: var(--grid-background-color);
    border-radius: 6px;
    padding: var(--grid-cell-gap);
    width: calc(var(--grid-cell-size) * 4 + var(--grid-cell-gap) * 5);
    margin: 0 auto;
}
.grid-container {
    position: absolute;
    z-index: 1;
}
.grid-row {
    display: flex;
    margin-bottom: var(--grid-cell-gap);
}
.grid-row:last-child {
    margin-bottom: 0;
}
.grid-cell {
    width: var(--grid-cell-size);
    height: var(--grid-cell-size);
    background-color: rgba(238, 228, 218, 0.35);
    border-radius: 3px;
}
/* 数字方块 */
.tile-container {
    position: absolute;
    z-index: 2;
    top: var(--grid-cell-gap);
    left: var(--grid-cell-gap);
}
.tile {
    position: absolute;
    width: var(--grid-cell-size);
    height: var(--grid-cell-size);
    font-size: 55px;
    font-weight: bold;
    line-height: var(--grid-cell-size);
    text-align: center;
    border-radius: 3px;
    transition: all 0.15s ease-in-out;
    z-index: 10;
}
.tile-2 { background-color: var(--tile-color-2); color: #776e65; }
.tile-4 { background-color: var(--tile-color-4); color: #776e65; }
.tile-8 { background-color: var(--tile-color-8); color: #f9f6f2; }
.tile-16 { background-color: var(--tile-color-16); color: #f9f6f2; }
.tile-32 { background-color: var(--tile-color-32); color: #f9f6f2; }
.tile-64 { background-color: var(--tile-color-64); color: #f9f6f2; }
.tile-128 { background-color: var(--tile-color-128); color: #f9f6f2; font-size: 45px; }
.tile-256 { background-color: var(--tile-color-256); color: #f9f6f2; font-size: 45px; }
.tile-512 { background-color: var(--tile-color-512); color: #f9f6f2; font-size: 45px; }
.tile-1024 { background-color: var(--tile-color-1024); color: #f9f6f2; font-size: 35px; }
.tile-2048 { background-color: var(--tile-color-2048); 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, .game-message-game-over {
    display: flex;
}
.game-message p {
    font-size: 60px;
    font-weight: bold;
    margin-bottom: 20px;
}
.game-won p { color: #f9f6f2; }
.game-over p { color: #776e65; }
.lower {
    display: flex;
    gap: 10px;
}
a.keep-playing-button, a.retry-button, button.new-game-button {
    background: #8f7a66;
    border-radius: 3px;
    padding: 0 20px;
    text-decoration: none;
    color: #f9f6f2;
    height: 40px;
    line-height: 40px;
    display: inline-block;
    text-align: center;
    font-weight: bold;
    cursor: pointer;
    border: none;
    font-size: 18px;
}
a.keep-playing-button:hover, a.retry-button:hover, button.new-game-button:hover {
    background: #9f8b77;
}
.game-explanation {
    margin-top: 30px;
    line-height: 1.5;
    font-size: 16px;
}

JavaScript 游戏逻辑

这是游戏的核心部分,我们将在这里实现所有功能。

script.js

js开发2048网页小游戏
(图片来源网络,侵删)
document.addEventListener('DOMContentLoaded', () => {
    // 游戏配置
    const GRID_SIZE = 4;
    const CELL_SIZE = 100;
    const CELL_GAP = 10;
    // DOM 元素
    const tileContainer = document.getElementById('tile-container');
    const scoreDisplay = document.getElementById('score');
    const bestScoreDisplay = document.getElementById('best-score');
    const newGameButton = document.getElementById('new-game-button');
    const gameMessage = document.querySelector('.game-message');
    const messageText = gameMessage.querySelector('p');
    const retryButton = document.querySelector('.retry-button');
    // 游戏状态
    let grid = [];
    let score = 0;
    let bestScore = localStorage.getItem('bestScore') || 0;
    bestScoreDisplay.textContent = bestScore;
    // 初始化游戏
    function initGame() {
        grid = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0));
        score = 0;
        scoreDisplay.textContent = score;
        gameMessage.classList.remove('game-won', 'game-over');
        tileContainer.innerHTML = '';
        addNewTile();
        addNewTile();
        updateDisplay();
    }
    // 在空白格子中随机添加一个新方块 (2 or 4)
    function addNewTile() {
        const emptyCells = [];
        for (let r = 0; r < GRID_SIZE; r++) {
            for (let c = 0; c < GRID_SIZE; c++) {
                if (grid[r][c] === 0) {
                    emptyCells.push({ r, c });
                }
            }
        }
        if (emptyCells.length > 0) {
            const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
            grid[randomCell.r][randomCell.c] = Math.random() < 0.9 ? 2 : 4;
        }
    }
    // 更新整个游戏盘面的显示
    function updateDisplay() {
        tileContainer.innerHTML = '';
        for (let r = 0; r < GRID_SIZE; r++) {
            for (let c = 0; c < GRID_SIZE; c++) {
                if (grid[r][c] !== 0) {
                    const tile = document.createElement('div');
                    tile.className = `tile tile-${grid[r][c]}`;
                    tile.textContent = grid[r][c];
                    tile.style.left = `${c * (CELL_SIZE + CELL_GAP)}px`;
                    tile.style.top = `${r * (CELL_SIZE + CELL_GAP)}px`;
                    tileContainer.appendChild(tile);
                }
            }
        }
    }
    // 移动逻辑
    function move(direction) {
        let moved = false;
        const previousGrid = grid.map(row => [...row]); // 深拷贝当前状态
        if (direction === 'ArrowLeft') {
            moved = moveLeft();
        } else if (direction === 'ArrowRight') {
            moved = moveRight();
        } else if (direction === 'ArrowUp') {
            moved = moveUp();
        } else if (direction === 'ArrowDown') {
            moved = moveDown();
        }
        if (moved) {
            addNewTile();
            updateDisplay();
            checkGameState();
        }
    }
    // 向左移动
    function moveLeft() {
        let moved = false;
        for (let r = 0; r < GRID_SIZE; r++) {
            const row = grid[r].filter(val => val !== 0);
            for (let c = 0; c < row.length - 1; c++) {
                if (row[c] === row[c + 1]) {
                    row[c] *= 2;
                    score += row[c];
                    scoreDisplay.textContent = score;
                    row.splice(c + 1, 1);
                }
            }
            while (row.length < GRID_SIZE) {
                row.push(0);
            }
            if (JSON.stringify(grid[r]) !== JSON.stringify(row)) {
                moved = true;
            }
            grid[r] = row;
        }
        return moved;
    }
    // 向右移动
    function moveRight() {
        let moved = false;
        for (let r = 0; r < GRID_SIZE; r++) {
            const row = grid[r].filter(val => val !== 0);
            for (let c = row.length - 1; c > 0; c--) {
                if (row[c] === row[c - 1]) {
                    row[c] *= 2;
                    score += row[c];
                    scoreDisplay.textContent = score;
                    row.splice(c - 1, 1);
                    c--; // 因为删除了一个元素,索引需要回退
                }
            }
            while (row.length < GRID_SIZE) {
                row.unshift(0);
            }
            if (JSON.stringify(grid[r]) !== JSON.stringify(row)) {
                moved = true;
            }
            grid[r] = row;
        }
        return moved;
    }
    // 向上移动
    function moveUp() {
        let moved = false;
        for (let c = 0; c < GRID_SIZE; c++) {
            const column = [];
            for (let r = 0; r < GRID_SIZE; r++) {
                if (grid[r][c] !== 0) {
                    column.push(grid[r][c]);
                }
            }
            for (let r = 0; r < column.length - 1; r++) {
                if (column[r] === column[r + 1]) {
                    column[r] *= 2;
                    score += column[r];
                    scoreDisplay.textContent = score;
                    column.splice(r + 1, 1);
                }
            }
            while (column.length < GRID_SIZE) {
                column.push(0);
            }
            for (let r = 0; r < GRID_SIZE; r++) {
                if (grid[r][c] !== column[r]) {
                    moved = true;
                }
                grid[r][c] = column[r];
            }
        }
        return moved;
    }
    // 向下移动
    function moveDown() {
        let moved = false;
        for (let c = 0; c < GRID_SIZE; c++) {
            const column = [];
            for (let r = 0; r < GRID_SIZE; r++) {
                if (grid[r][c] !== 0) {
                    column.push(grid[r][c]);
                }
            }
            for (let r = column.length - 1; r > 0; r--) {
                if (column[r] === column[r - 1]) {
                    column[r] *= 2;
                    score += column[r];
                    scoreDisplay.textContent = score;
                    column.splice(r - 1, 1);
                    r--;
                }
            }
            while (column.length < GRID_SIZE) {
                column.unshift(0);
            }
            for (let r = 0; r < GRID_SIZE; r++) {
                if (grid[r][c] !== column[r]) {
                    moved = true;
                }
                grid[r][c] = column[r];
            }
        }
        return moved;
    }
    // 检查游戏状态 (胜利或失败)
    function checkGameState() {
        // 检查是否获胜 (出现 2048)
        for (let r = 0; r < GRID_SIZE; r++) {
            for (let c = 0; c < GRID_SIZE; c++) {
                if (grid[r][c] === 2048) {
                    gameMessage.classList.add('game-won');
                    messageText.textContent = '你赢了!';
                    return;
                }
            }
        }
        // 检查是否还有空格
        for (let r = 0; r < GRID_SIZE; r++) {
            for (let c = 0; c < GRID_SIZE; c++) {
                if (grid[r][c] === 0) {
                    return; // 游戏继续
                }
            }
        }
        // 检查是否还能移动
        for (let r = 0; r < GRID_SIZE; r++) {
            for (let c = 0; c < GRID_SIZE; c++) {
                const current = grid[r][c];
                // 检查右边
                if (c < GRID_SIZE - 1 && grid[r][c + 1] === current) {
                    return; // 可以合并,游戏继续
                }
                // 检查下边
                if (r < GRID_SIZE - 1 && grid[r + 1][c] === current) {
                    return; // 可以合并,游戏继续
                }
            }
        }
        // 如果以上条件都不满足,则游戏结束
        gameMessage.classList.add('game-over');
        messageText.textContent = '游戏结束!';
        // 更新最高分
        if (score > bestScore) {
            bestScore = score;
            bestScoreDisplay.textContent = bestScore;
            localStorage.setItem('bestScore', bestScore);
        }
    }
    // 事件监听
    document.addEventListener('keydown', (event) => {
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
            event.preventDefault();
            move(event.key);
        }
    });
    newGameButton.addEventListener('click', initGame);
    retryButton.addEventListener('click', initGame);
    // 启动游戏
    initGame();
});

如何运行

  1. 将以上三段代码分别保存为 index.html, style.css, 和 script.js
  2. 确保这三个文件在同一个文件夹中。
  3. 用浏览器打开 index.html 文件。

你应该能看到一个可以玩的 2048 游戏了!你可以使用键盘方向键来控制方块移动和合并,祝你玩得开心!

js开发2048网页小游戏
(图片来源网络,侵删)