
1. 引言拼图游戏是一种经典的益智游戏它将一张完整的图片分割成若干小块玩家需要通过拖动和旋转这些小碎片将它们重新组合成完整的图片。这种游戏不仅能够锻炼玩家的空间想象力和逻辑思维能力还能带来完成挑战后的成就感。随着前端技术的发展使用 HTML、CSS 和 JavaScript 在浏览器中实现一个交互式的拼图游戏已经变得非常简单。本文将带领大家从零开始一步步实现一个功能完整的拼图小游戏涵盖图片分割、碎片拖动、自动吸附、计时和计步等核心功能。2. 项目结构与准备首先我们创建一个简单的项目结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title拼图小游戏/title style /* 样式将在后续步骤中添加 */ /style /head body div idgame-container h1拼图小游戏/h1 div idcontrols button idstart-btn开始游戏/button button idreset-btn重置/button span idtimer时间: 00:00/span span idsteps步数: 0/span /div div idpuzzle-container/div div idoriginal-image-container h3原图参考/h3 img idoriginal-image srcpuzzle-image.jpg alt拼图原图 /div /div script // JavaScript 代码将在后续步骤中添加 /script /body /html准备一张用于拼图的图片例如puzzle-image.jpg并将其放在项目目录中。3. 核心功能实现3.1 图片分割与碎片生成我们需要将原图分割成 3x3 或 4x4 的网格并生成对应的碎片元素。以下是实现这一功能的 JavaScript 代码class PuzzleGame { constructor(containerId, imageSrc, rows 3, cols 3) { this.container document.getElementById(containerId); this.imageSrc imageSrc; this.rows rows; this.cols cols; this.totalPieces rows * cols; this.pieces []; this.emptyIndex this.totalPieces - 1; // 最后一个位置作为空位 this.steps 0; this.time 0; this.timerInterval null; this.isPlaying false; this.init(); } init() { this.createPuzzleContainer(); this.loadImageAndCreatePieces(); this.setupEventListeners(); } createPuzzleContainer() { // 创建拼图容器 const puzzleContainer document.createElement(div); puzzleContainer.id puzzle-board; puzzleContainer.style.display grid; puzzleContainer.style.gridTemplateColumns repeat(${this.cols}, 1fr); puzzleContainer.style.gridTemplateRows repeat(${this.rows}, 1fr); puzzleContainer.style.width 400px; puzzleContainer.style.height 400px; puzzleContainer.style.border 2px solid #333; puzzleContainer.style.margin 20px auto; this.container.appendChild(puzzleContainer); this.board puzzleContainer; } loadImageAndCreatePieces() { const img new Image(); img.src this.imageSrc; img.onload () { this.image img; this.createPieces(); this.shufflePieces(); }; } createPieces() { const pieceWidth this.image.width / this.cols; const pieceHeight this.image.height / this.rows; for (let row 0; row this.rows; row) { for (let col 0; col this.cols; col) { const pieceIndex row * this.cols col; const isLastPiece pieceIndex this.emptyIndex; const piece document.createElement(div); piece.className puzzle-piece; piece.dataset.index pieceIndex; piece.dataset.correctRow row; piece.dataset.correctCol col; if (!isLastPiece) { // 创建 canvas 来绘制图片片段 const canvas document.createElement(canvas); const ctx canvas.getContext(2d); canvas.width pieceWidth; canvas.height pieceHeight; // 绘制图片的对应部分 ctx.drawImage( this.image, col * pieceWidth, row * pieceHeight, pieceWidth, pieceHeight, 0, 0, pieceWidth, pieceHeight ); piece.style.backgroundImage url(${canvas.toDataURL()}); piece.style.backgroundSize ${this.cols * 100}% ${this.rows * 100}%; piece.style.backgroundPosition -${col * pieceWidth}px -${row * pieceHeight}px; // 添加拖动功能 piece.draggable true; } else { // 最后一个碎片作为空位 piece.className puzzle-piece empty; piece.style.backgroundColor #f0f0f0; } piece.style.width 100%; piece.style.height 100%; piece.style.border 1px solid #ddd; piece.style.boxSizing border-box; piece.style.cursor grab; piece.style.userSelect none; this.board.appendChild(piece); this.pieces.push(piece); } } } }3.2 碎片拖动与交换逻辑接下来实现碎片的拖动功能和与相邻空位的交换逻辑// 在 PuzzleGame 类中继续添加方法 shufflePieces() { // 实现洗牌算法Fisher-Yates shuffle const indices Array.from({length: this.totalPieces - 1}, (_, i) i); for (let i indices.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [indices[i], indices[j]] [indices[j], indices[i]]; } // 将洗牌后的碎片重新排列最后一个位置保持为空 indices.push(this.emptyIndex); // 清空棋盘并重新添加碎片 this.board.innerHTML ; indices.forEach(index { this.board.appendChild(this.pieces[index]); }); this.updatePiecePositions(); } setupEventListeners() { this.board.addEventListener(dragstart, (e) { if (e.target.classList.contains(puzzle-piece) !e.target.classList.contains(empty)) { e.dataTransfer.setData(text/plain, e.target.dataset.index); e.target.style.opacity 0.5; } }); this.board.addEventListener(dragover, (e) { e.preventDefault(); }); this.board.addEventListener(drop, (e) { e.preventDefault(); const draggedIndex e.dataTransfer.getData(text/plain); const targetPiece e.target.closest(.puzzle-piece); if (!targetPiece || !draggedIndex) return; const draggedPiece this.pieces[draggedIndex]; const targetIndex Array.from(this.board.children).indexOf(targetPiece); const draggedPieceIndex Array.from(this.board.children).indexOf(draggedPiece); // 检查是否相邻 if (this.areAdjacent(draggedPieceIndex, targetIndex)) { this.swapPieces(draggedPieceIndex, targetIndex); this.steps; this.updateStepsDisplay(); if (this.checkWin()) { this.gameWin(); } } draggedPiece.style.opacity 1; }); this.board.addEventListener(dragend, (e) { if (e.target.classList.contains(puzzle-piece)) { e.target.style.opacity 1; } }); } areAdjacent(index1, index2) { const row1 Math.floor(index1 / this.cols); const col1 index1 % this.cols; const row2 Math.floor(index2 / this.cols); const col2 index2 % this.cols; // 检查是否相邻上下左右 return (Math.abs(row1 - row2) 1 col1 col2) || (Math.abs(col1 - col2) 1 row1 row2); } swapPieces(index1, index2) { const children Array.from(this.board.children); const temp children[index1]; children[index1] children[index2]; children[index2] temp; // 更新 DOM this.board.innerHTML ; children.forEach(child this.board.appendChild(child)); this.updatePiecePositions(); } updatePiecePositions() { const children Array.from(this.board.children); children.forEach((piece, index) { piece.dataset.currentIndex index; }); }3.3 游戏状态与界面更新实现计时器、步数统计和游戏胜利检测// 在 PuzzleGame 类中继续添加方法 startGame() { if (this.isPlaying) return; this.isPlaying true; this.steps 0; this.time 0; this.updateStepsDisplay(); this.updateTimerDisplay(); this.timerInterval setInterval(() { this.time; this.updateTimerDisplay(); }, 1000); this.shufflePieces(); } resetGame() { this.isPlaying false; clearInterval(this.timerInterval); this.steps 0; this.time 0; this.updateStepsDisplay(); this.updateTimerDisplay(); this.resetPieces(); } resetPieces() { // 将碎片按正确顺序排列 const sortedPieces [...this.pieces].sort((a, b) { return parseInt(a.dataset.index) - parseInt(b.dataset.index); }); this.board.innerHTML ; sortedPieces.forEach(piece { this.board.appendChild(piece); }); this.updatePiecePositions(); } updateStepsDisplay() { const stepsElement document.getElementById(steps); if (stepsElement) { stepsElement.textContent 步数: ${this.steps}; } } updateTimerDisplay() { const timerElement document.getElementById(timer); if (timerElement) { const minutes Math.floor(this.time / 60).toString().padStart(2, 0); const seconds (this.time % 60).toString().padStart(2, 0); timerElement.textContent 时间: ${minutes}:${seconds}; } } checkWin() { const children Array.from(this.board.children); for (let i 0; i children.length - 1; i) { const piece children[i]; const currentIndex parseInt(piece.dataset.currentIndex); const correctIndex parseInt(piece.dataset.index); if (currentIndex ! correctIndex) { return false; } } return true; } gameWin() { this.isPlaying false; clearInterval(this.timerInterval); alert(恭喜你完成了拼图\n用时: ${this.formatTime(this.time)}\n步数: ${this.steps}); } formatTime(seconds) { const minutes Math.floor(seconds / 60); const remainingSeconds seconds % 60; return ${minutes.toString().padStart(2, 0)}:${remainingSeconds.toString().padStart(2, 0)}; }4. 完整样式与交互优化添加完整的 CSS 样式让游戏界面更加美观#game-container { max-width: 800px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; text-align: center; } #controls { margin: 20px 0; display: flex; justify-content: center; gap: 20px; align-items: center; } button { padding: 10px 20px; font-size: 16px; background-color: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s; } button:hover { background-color: #45a049; } #reset-btn { background-color: #f44336; } #reset-btn:hover { background-color: #d32f2f; } #timer, #steps { font-size: 18px; font-weight: bold; color: #333; padding: 10px; background-color: #f5f5f5; border-radius: 5px; min-width: 120px; display: inline-block; } #puzzle-board { width: 400px; height: 400px; border: 2px solid #333; margin: 20px auto; background-color: #f9f9f9; box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .puzzle-piece { transition: transform 0.2s, opacity 0.2s; } .puzzle-piece:hover:not(.empty) { transform: scale(1.02); box-shadow: 0 2px 4px rgba(0,0,0,0.2); z-index: 1; } .puzzle-piece.dragging { opacity: 0.7; } #original-image-container { margin-top: 30px; padding: 20px; background-color: #f5f5f5; border-radius: 10px; } #original-image { max-width: 200px; max-height: 200px; border: 2px solid #ddd; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #333; margin-bottom: 10px; } h3 { color: #666; margin-bottom: 10px; }5. 游戏初始化与功能扩展最后初始化游戏并添加一些扩展功能// 页面加载完成后初始化游戏 document.addEventListener(DOMContentLoaded, () { const game new PuzzleGame(puzzle-container, puzzle-image.jpg, 3, 3); // 绑定按钮事件 document.getElementById(start-btn).addEventListener(click, () { game.startGame(); }); document.getElementById(reset-btn).addEventListener(click, () { game.resetGame(); }); // 添加难度选择 const difficultySelect document.createElement(select); difficultySelect.id difficulty-select; difficultySelect.innerHTML option value3简单 (3x3)/option option value4中等 (4x4)/option option value5困难 (5x5)/option ; difficultySelect.addEventListener(change, (e) { const size parseInt(e.target.value); game.resetGame(); // 这里可以重新初始化游戏但需要重新加载图片 // 实际项目中可能需要更复杂的重新初始化逻辑 }); document.getElementById(controls).appendChild(difficultySelect); // 添加提示功能 const hintBtn document.createElement(button); hintBtn.id hint-btn; hintBtn.textContent 提示; hintBtn.addEventListener(click, () { // 实现提示逻辑高亮可以移动的碎片 const emptyIndex game.emptyIndex; const children Array.from(game.board.children); children.forEach((piece, index) { if (game.areAdjacent(index, emptyIndex) !piece.classList.contains(empty)) { piece.style.boxShadow 0 0 10px yellow; setTimeout(() { piece.style.boxShadow ; }, 1000); } }); }); document.getElementById(controls).appendChild(hintBtn); }); // 添加触摸屏支持 PuzzleGame.prototype.setupTouchEvents function() { this.board.addEventListener(touchstart, (e) { const touch e.touches[0]; const piece document.elementFromPoint(touch.clientX, touch.clientY); if (piece piece.classList.contains(puzzle-piece) !piece.classList.contains(empty)) { this.touchDraggedPiece