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