通过html、js、css实现一个简单的2048小游戏(附源码)

1. 效果展示

2. 核心功能实现讲解

核心逻辑都在script.js中。主要包含以下几个部分:

  • 游戏初始化
  • 网格管理(移动、合并)
  • 事件处理
  • 用户界面更新

a. 初始化游戏

在开始游戏时,我们需要创建一个4x4的空网格,并随机添加两个数字来启动游戏。

class Game2048 {
    constructor() {
        this.grid = Array(4).fill().map(() => Array(4).fill(0));
        this.score = 0;
        // 初始化时添加两个数字
        this.addNumber();
        this.addNumber();
        this.bindEvents();
        this.updateDisplay();
    }

    addNumber() {
        const emptyCells = [];
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                if (this.grid[i][j] === 0) {
                    emptyCells.push({x: i, y: j});
                }
            }
        }
        const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
        this.grid[randomCell.x][randomCell.y] = Math.random() < 0.9 ? 2 : 4;
    }

    bindEvents() {
        document.addEventListener('keydown', (e) => this.handleKeyPress(e));
    }
}

这里,我们定义了一个Game2048类,负责管理游戏的状态和行为。初始化时创建一个4x4的二维数组作为网格,并随机添加两个数字(通常是2或4)。

b. 处理键盘事件

玩家通过方向键来控制方块的移动。我们需要监听页面上的KeyPress事件,并根据按键的方向调用相应的移动方法。

handleKeyPress(e) {
    switch(e.key) {
        case 'ArrowUp':
            this.move('up');
            break;
        case 'ArrowDown':
            this.move('down');
            break;
        case 'ArrowLeft':
            this.move('left');
            break;
        case 'ArrowRight':
            this.move('right');
            break;
    }
}

每个方向对应不同的移动逻辑,核心是调用move()方法处理网格的变化。

c. 移动与合并

移动的核心在于如何调整网格中的数字。每次移动时,我们需要将同一行或列的数字向指定的方向靠拢,并合并相同的数字。

move(direction) {
    let moved = false;
    const newGrid = Array(4).fill().map(() => Array(4).fill(0));

    switch(direction) {
        case 'left':
            for (let i = 0; i < 4; i++) {
                let row = this.grid[i].filter(cell => cell !== 0);
                // 合并
                for (let j = 0; j < row.length - 1; j++) {
                    if (row[j] === row[j + 1]) {
                        row[j] *= 2;
                        this.score += row[j];
                        row.splice(j + 1, 1);
                    }
                }
                // 填充空隙
                while (row.length < 4) {
                    row.push(0);
                }
                newGrid[i] = row;
            }
            break;
        // 其他方向的处理类似,通过转置或反转数组来统一逻辑
    }

    if (! isEqual(this.grid, newGrid)) {
        moved = true;
        this.addNumber();
    }
}

这里,move()方法根据不同的方向调整网格中的数字。以左移为例,首先过滤掉空隙(0),然后合并相邻的相同数字,并在末尾添加0来填充剩下的位置。

d. 更新显示

每次操作后,都需要更新用户界面,将最新的网格状态绘制出来。

updateDisplay() {
    const canvas = document.getElementById('grid');
    const ctx = canvas.getContext('2d');
    canvas.width = 400; // 假设每个单元是80px,总宽度为4 * 80 = 320px,加边距和样式调整
    canvas.height = 400;

    ctx.fillStyle = '#bbada0';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
            if (this.grid[i][j] !== 0) {
                const x = i * 80 + 10;
                const y = j * 80 + 10;
                ctx.fillStyle = getCellColor(this.grid[i][j]);
                ctx.fillRect(x, y, 60, 60);
                ctx.fillStyle = 'black';
                ctx.font = '24px Arial';
                ctx.fillText(this.grid[i][j], x + 30, y + 55);
            }
        }
    }

    document.getElementById('score').textContent = this.score;
}

这部分代码负责将网格中的数字绘制到Canvas上,包括颜色填充和文字显示。

3. 源码文件

  • index.html: 包含HTML结构和基础设置。
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>2048 Game</title>
 <link rel="stylesheet" href="style.css">
</head>
<body>
 <div class="container">
 <div class="header">
 <h1>2048</h1>
 <div class="score-container">
                Score: <span id="score">0</span>
 </div>
 <button id="new-game">New Game</button>
 </div>
 <div class="grid-container">
 <div class="grid-row">
 <div class="grid-cell" id="cell-0-0"></div>
 <div class="grid-cell" id="cell-0-1"></div>
 <div class="grid-cell" id="cell-0-2"></div>
 <div class="grid-cell" id="cell-0-3"></div>
 </div>
 <div class="grid-row">
 <div class="grid-cell" id="cell-1-0"></div>
 <div class="grid-cell" id="cell-1-1"></div>
 <div class="grid-cell" id="cell-1-2"></div>
 <div class="grid-cell" id="cell-1-3"></div>
 </div>
 <div class="grid-row">
 <div class="grid-cell" id="cell-2-0"></div>
 <div class="grid-cell" id="cell-2-1"></div>
 <div class="grid-cell" id="cell-2-2"></div>
 <div class="grid-cell" id="cell-2-3"></div>
 </div>
 <div class="grid-row">
 <div class="grid-cell" id="cell-3-0"></div>
 <div class="grid-cell" id="cell-3-1"></div>
 <div class="grid-cell" id="cell-3-2"></div>
 <div class="grid-cell" id="cell-3-3"></div>
 </div>
 </div>
 </div>
 <script src="script.js"></script>
</body>
</html> 
  • script.js: 用于实现游戏逻辑的JavaScript代码。
class Game2048 {
 constructor() {
 this.grid = Array(4).fill().map(() => Array(4).fill(0));
 this.score = 0;
 this.init();
    }

 init() {
 // Clear grid and score
 this.grid = Array(4).fill().map(() => Array(4).fill(0));
 this.score = 0;
 document.getElementById('score').textContent = this.score;
 
 // Add two initial numbers
 this.addNewNumber();
 this.addNewNumber();
 this.updateView();
    }

 addNewNumber() {
 // Get all empty cells
 const emptyCells = [];
 for (let i = 0; i < 4; i++) {
 for (let j = 0; j < 4; j++) {
 if (this.grid[i][j] === 0) {
 emptyCells.push({x: i, y: j});
                }
            }
        }

 if (emptyCells.length > 0) {
 // Randomly select an empty cell
 const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
 // 90% chance for 2, 10% chance for 4
 this.grid[randomCell.x][randomCell.y] = Math.random() < 0.9 ? 2 : 4;
        }
    }

 updateView() {
 for (let i = 0; i < 4; i++) {
 for (let j = 0; j < 4; j++) {
 const cell = document.getElementById(`cell-${i}-${j}`);
 const value = this.grid[i][j];
 cell.textContent = value || '';
 cell.setAttribute('data-value', value);
            }
        }
 document.getElementById('score').textContent = this.score;
    }

 move(direction) {
 let moved = false;
 const originalGrid = JSON.stringify(this.grid);

 // Handle array based on direction
 if (direction === 'left' || direction === 'right') {
 for (let i = 0; i < 4; i++) {
 let row = this.grid[i].slice();
 if (direction === 'right') row.reverse();
 
 // Merge same numbers
 let newRow = this.mergeLine(row);
 
 if (direction === 'right') newRow.reverse();
 this.grid[i] = newRow;
            }
        } else {
 for (let j = 0; j < 4; j++) {
 let column = this.grid.map(row => row[j]);
 if (direction === 'down') column.reverse();
 
 // Merge same numbers
 let newColumn = this.mergeLine(column);
 
 if (direction === 'down') newColumn.reverse();
 for (let i = 0; i < 4; i++) {
 this.grid[i][j] = newColumn[i];
                }
            }
        }

 // Check if there was any movement
 if (originalGrid !== JSON.stringify(this.grid)) {
 this.addNewNumber();
 this.updateView();
 
 // Check if game is over
 if (this.isGameOver()) {
 setTimeout(() => {
 alert('Game Over! Your score: ' + this.score);
                }, 300);
            }
        }
    }

 mergeLine(line) {
 // Remove empty cells
 let newLine = line.filter(num => num !== 0);
 
 // Merge same numbers
 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);
            }
        }
 
 // Fill with zeros
 while (newLine.length < 4) {
 newLine.push(0);
        }
 
 return newLine;
    }

 isGameOver() {
 // Check for empty cells
 for (let i = 0; i < 4; i++) {
 for (let j = 0; j < 4; j++) {
 if (this.grid[i][j] === 0) return false;
            }
        }

 // Check for possible merges
 for (let i = 0; i < 4; i++) {
 for (let j = 0; j < 3; j++) {
 if (this.grid[i][j] === this.grid[i][j + 1]) return false;
 if (this.grid[j][i] === this.grid[j + 1][i]) return false;
            }
        }

 return true;
    }
}

// Initialize game
const game = new Game2048();

// Add keyboard event listener
document.addEventListener('keydown', (event) => {
 switch(event.key) {
 case 'ArrowLeft':
 game.move('left');
 break;
 case 'ArrowRight':
 game.move('right');
 break;
 case 'ArrowUp':
 game.move('up');
 break;
 case 'ArrowDown':
 game.move('down');
 break;
    }
});

// Add new game button event listener
document.getElementById('new-game').addEventListener('click', () => {
 game.init();
}); 
  • style.css: 定义游戏界面的样式和美观。
* {
 box-sizing: border-box;
 margin: 0;
 padding: 0;
}

body {
 background-color: #faf8ef;
 font-family: Arial, sans-serif;
}

.container {
 width: 460px;
 margin: 40px auto;
}

.header {
 display: flex;
 justify-content: space-between;
 align-items: center;
 margin-bottom: 20px;
}

h1 {
 color: #776e65;
 font-size: 48px;
}

.score-container {
 background: #bbada0;
 padding: 15px 25px;
 border-radius: 6px;
 color: white;
 font-size: 18px;
}

#new-game {
 background: #8f7a66;
 color: white;
 border: none;
 padding: 10px 20px;
 border-radius: 6px;
 cursor: pointer;
 font-size: 16px;
 transition: background-color 0.3s;
}

#new-game:hover {
 background: #7f6a56;
}

.grid-container {
 background: #bbada0;
 padding: 15px;
 border-radius: 6px;
}

.grid-row {
 display: flex;
 margin-bottom: 15px;
}

.grid-row:last-child {
 margin-bottom: 0;
}

.grid-cell {
 width: 100px;
 height: 100px;
 margin-right: 15px;
 background: rgba(238, 228, 218, 0.35);
 border-radius: 3px;
 display: flex;
 justify-content: center;
 align-items: center;
 font-size: 36px;
 font-weight: bold;
 color: #776e65;
 transition: all 0.15s ease;
}

.grid-cell:last-child {
 margin-right: 0;
}

.grid-cell[data-value="2"] {
 background: #eee4da;
}

.grid-cell[data-value="4"] {
 background: #ede0c8;
}

.grid-cell[data-value="8"] {
 background: #f2b179;
 color: white;
}

.grid-cell[data-value="16"] {
 background: #f59563;
 color: white;
}

.grid-cell[data-value="32"] {
 background: #f67c5f;
 color: white;
}

.grid-cell[data-value="64"] {
 background: #f65e3b;
 color: white;
}

.grid-cell[data-value="128"] {
 background: #edcf72;
 color: white;
 font-size: 32px;
}

.grid-cell[data-value="256"] {
 background: #edcc61;
 color: white;
 font-size: 32px;
}

.grid-cell[data-value="512"] {
 background: #edc850;
 color: white;
 font-size: 32px;
}

.grid-cell[data-value="1024"] {
 background: #edc53f;
 color: white;
 font-size: 28px;
}

.grid-cell[data-value="2048"] {
 background: #edc22e;
 color: white;
 font-size: 28px;
} 

 附链接https://zhuanlan.zhihu.com/p/1907398664005068340

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值