简介:这是一套基于HTML实现的拼图游戏1.0完整设计源码,面向前端初学者与全栈入门开发者,用于实践静态页面搭建、交互逻辑开发及前后端协同设计。资源共144个文件,压缩包大小1.62MB,涵盖75个GIF动图(支撑拼图块动态展示与反馈)、23个JavaScript脚本(实现拖拽、校验、计时等核心交互)、7个HTML页面(含主界面、帮助页与排行榜)、5个CSS样式表(统一视觉风格与响应式布局),以及PHP后端脚本、Java类与配置文件,体现轻量级前后端联动架构。内容预览可见layui、layer等主流UI框架CSS,以及mvnw.cmd、gitignore等工程化配置文件,说明项目具备可构建、可调试的完整开发环境特征。已有273人学习下载,读者可直接运行调试、理解模块划分逻辑、复用动画资源与交互组件,快速掌握网页游戏开发全流程。
1. 用纯 HTML/CSS/JS 实现可交互拼图游戏:零依赖、可嵌入、适合教学与作品集展示
你不需要 Node.js 环境,不依赖任何构建工具,甚至不用本地服务器——只要一个.html文件,双击就能在 Chrome/Firefox/Edge 中运行一个完整可拖拽、可点击交换、带计时与步数统计的拼图游戏。这不是玩具级 demo,而是真实项目中常被复用的「HTML 前端交互最小闭环」:DOM 动态生成格子、CSS Grid 精确控制布局、JavaScript 实现空格识别与邻接交换逻辑、localStorage 持久化最高分。它特别适合前端初学者练手(理解document.createElement与事件委托)、教学演示(讲清楚><!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="基于纯 HTML/CSS/JS 的可交互拼图游戏,支持 3×3 与 4×4 规格,含计时与步数统计"> <title>HTML 拼图游戏 1.0</title> <!-- 内联样式与脚本将在此处插入 --> </head> <body> <!-- 游戏容器 --> </body> </html>
提示:
<meta name="viewport">对移动端适配至关重要。若省略,iOS Safari 会默认按 980px 宽度渲染,导致游戏区域被压缩变形。此处initial-scale=1.0确保像素级精准缩放。
2.2 游戏主容器的 DOM 结构设计原则
容器结构需兼顾可维护性与性能。我们采用单层嵌套:外层#game-container包裹状态栏与棋盘,内部#puzzle-board使用<section>语义化标签而非<div>,体现其为独立功能区块。每个拼图块使用<button>而非<div>,天然支持键盘焦点与空格/回车触发,无需额外tabindex或role属性——这是符合 WCAG 2.1 的最小化无障碍实践。
<div id="game-container"> <div id="status-bar"> <span>步数:<span id="move-count">0</span></span> <span>用时:<span id="timer">00:00</span></span> <button id="reset-btn">重新开始</button> </div> <section id="puzzle-board" role="grid" aria-label="3×3 拼图游戏棋盘"></section> </div>2.2.1role="grid"与aria-label的实际作用
role="grid"告知辅助技术这是一个表格型布局,配合后续为每个按钮添加role="gridcell"和aria-rowindex/aria-colindex,可让视障用户通过方向键导航格子。aria-label则提供上下文,避免屏幕阅读器仅读出“section”而不知用途。实测中,NVDA + Firefox 组合下,用户能准确听到“3×3 拼图游戏棋盘”,并用箭头键逐格移动。
2.3 样式隔离策略:CSS 自包含与响应式断点
所有样式必须内联于<style>标签中,避免外部文件请求失败导致界面崩溃。关键策略是使用 CSS 自定义属性(CSS Variables)集中管理尺寸与颜色,便于后续修改:
<style> :root { --grid-size: 3; --cell-size: 100px; --gap: 4px; --bg-color: #f5f5f5; --border-color: #ddd; } @media (max-width: 600px) { :root { --cell-size: 70px; } } #puzzle-board { display: grid; grid-template-columns: repeat(var(--grid-size), 1fr); gap: var(--gap); width: calc(var(--grid-size) * var(--cell-size) + (var(--grid-size) - 1) * var(--gap)); margin: 0 auto; } button.puzzle-piece { width: var(--cell-size); height: var(--cell-size); font-size: 18px; font-weight: bold; border: 1px solid var(--border-color); background: white; cursor: pointer; transition: transform 0.2s ease; } button.puzzle-piece:hover:not(:disabled) { transform: scale(1.05); } button.puzzle-piece:disabled { background: var(--bg-color); cursor: default; } </style>注意:
grid-template-columns: repeat(var(--grid-size), 1fr)是核心。当切换为 4×4 模式时,只需动态修改--grid-size变量值(如document.documentElement.style.setProperty('--grid-size', '4')),整个布局自动重排,无需重写 CSS。
3. 用 JavaScript 实现拼图核心逻辑:空格识别、邻接交换与状态校验
3.1 初始化棋盘:动态生成按钮并标记空位
游戏启动时,JavaScript 需生成n²个按钮(n为规格),其中n²-1个显示数字,1 个为空白。关键在于空位必须随机分布且保证有解。简单随机打乱会导致 50% 情况无解(奇偶性问题),因此采用「从终态反向移动」策略:先创建已排序数组[1,2,3,...,n²-1,''],再模拟 100 次合法空格移动(每次只与上/下/左/右相邻格交换),确保初始状态必然可解。
function initBoard(size) { const board = []; const total = size * size; // 创建有序数组,末尾为空 for (let i = 1; i < total; i++) board.push(i); board.push(''); // 反向随机移动 100 次 for (let i = 0; i < 100; i++) { const emptyIndex = board.indexOf(''); const candidates = getAdjacentIndices(emptyIndex, size); const swapIndex = candidates[Math.floor(Math.random() * candidates.length)]; [board[emptyIndex], board[swapIndex]] = [board[swapIndex], board[emptyIndex]]; } return board; } function getAdjacentIndices(index, size) { const row = Math.floor(index / size); const col = index % size; const candidates = []; if (row > 0) candidates.push(index - size); // 上 if (row < size - 1) candidates.push(index + size); // 下 if (col > 0) candidates.push(index - 1); // 左 if (col < size - 1) candidates.push(index + 1); // 右 return candidates; }3.1.1getAdjacentIndices的边界处理逻辑
该函数接收当前空格索引index和规格size,计算其所在行列后,仅添加合法邻接索引。例如 3×3 棋盘中索引 0(左上角)只有「下」和「右」两个邻接位(索引 3 和 1),函数严格排除index - size = -3和index - 1 = -1,避免数组越界。这是防止board[swapIndex]报错的关键守卫。
3.2 事件绑定:委托到棋盘容器,避免内存泄漏
为n²个按钮单独绑定事件监听器是低效且危险的(尤其频繁重置时)。正确做法是事件委托:监听#puzzle-board的click事件,通过event.target判断是否为.puzzle-piece按钮,并验证其是否与空格邻接。
document.getElementById('puzzle-board').addEventListener('click', function(e) { if (!e.target.classList.contains('puzzle-piece')) return; const clickedIndex = parseInt(e.target.dataset.index); const emptyIndex = getCurrentEmptyIndex(); // 检查是否邻接 if (isAdjacent(clickedIndex, emptyIndex, gridSize)) { swapPieces(clickedIndex, emptyIndex); updateMoveCount(); checkWinCondition(); } }); function isAdjacent(idx1, idx2, size) { const row1 = Math.floor(idx1 / size), col1 = idx1 % size; const row2 = Math.floor(idx2 / size), col2 = idx2 % size; return (Math.abs(row1 - row2) === 1 && col1 === col2) || (Math.abs(col1 - col2) === 1 && row1 === row2); }提示:
dataset.index是初始化时为每个按钮设置的>function checkWinCondition() { const board = getCurrentBoardState(); const total = gridSize * gridSize; let isWin = true; for (let i = 0; i < total - 1; i++) { if (board[i] !== i + 1) { isWin = false; break; } } if (board[total - 1] !== '') isWin = false; if (isWin) { const time = getTimeInSeconds(); const moves = moveCount; const key = `puzzle_${gridSize}x${gridSize}`; const best = JSON.parse(localStorage.getItem(key) || '{}'); const isNewBest = !best.time || time < best.time || (time === best.time && moves < (best.moves || Infinity)); if (isNewBest) { localStorage.setItem(key, JSON.stringify({ time, moves })); showWinMessage(`新纪录!${formatTime(time)},${moves} 步`); } else { showWinMessage(`恭喜完成!${formatTime(time)},${moves} 步`); } } }3.3.1
localStorage键名设计与数据格式键名
puzzle_3x3和puzzle_4x4确保不同规格成绩互不干扰。存储对象{ time: 123, moves: 45 }便于后续扩展(如增加「最快平均时间」统计)。JSON.parse(... || '{}')处理首次运行时getItem返回null的情况,避免Cannot read property 'time' of null错误。4. 支持 3×3 与 4×4 双规格切换:动态 DOM 重建与状态重置
4.1 规格切换按钮的 DOM 注入与事件绑定
在状态栏中添加规格选择控件,使用
<select>而非按钮组,语义更清晰且支持键盘操作。选项值直接对应gridSize数值,变更时触发resetGame(gridSize)。<div id="status-bar"> <span>步数:<span id="move-count">0</span></span> <span>用时:<span id="timer">00:00</span></span> <select id="size-selector"> <option value="3">3×3</option> <option value="4">4×4</option> </select> <button id="reset-btn">重新开始</button> </div>document.getElementById('size-selector').addEventListener('change', function() { gridSize = parseInt(this.value); resetGame(gridSize); });4.2
resetGame()的完整执行链此函数是游戏状态重置的核心,必须原子化执行:清除定时器、重置计数器、重建棋盘 DOM、重新初始化数组、绑定新事件。关键点在于完全销毁旧 DOM 节点,而非清空
innerHTML,避免残留事件监听器。function resetGame(size) { // 1. 清除定时器 if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } // 2. 重置状态 moveCount = 0; startTime = Date.now(); document.getElementById('move-count').textContent = '0'; document.getElementById('timer').textContent = '00:00'; // 3. 重建棋盘 DOM const boardEl = document.getElementById('puzzle-board'); boardEl.innerHTML = ''; // 彻底清空 boardEl.setAttribute('aria-label', `${size}×${size} 拼图游戏棋盘`); // 4. 生成新棋盘数据 currentBoard = initBoard(size); // 5. 动态创建按钮 currentBoard.forEach((value, index) => { const btn = document.createElement('button'); btn.className = 'puzzle-piece'; btn.textContent = value === '' ? '' : value; btn.dataset.index = index; btn.setAttribute('role', 'gridcell'); btn.setAttribute('aria-rowindex', Math.floor(index / size) + 1); btn.setAttribute('aria-colindex', (index % size) + 1); if (value === '') { btn.disabled = true; btn.style.background = '#f5f5f5'; } boardEl.appendChild(btn); }); // 6. 启动新计时器 startTime = Date.now(); timerInterval = setInterval(updateTimer, 1000); }4.2.1
aria-rowindex与aria-colindex的动态赋值这两个属性必须随
index实时计算:Math.floor(index / size) + 1得到行号(从 1 开始),(index % size) + 1得到列号。屏幕阅读器会读出「第2行第3列,按钮,内容为5」,使导航具备空间感。若固定写死则无法适配不同规格。4.3 响应式适配:CSS Grid 与媒体查询协同
当规格从 3×3 切换到 4×4,棋盘宽度从
3*100px+2*4px=308px变为4*100px+3*4px=412px,可能超出小屏宽度。此时@media (max-width: 600px)规则生效,将--cell-size从100px降为70px,同时#puzzle-board的width计算式自动适应新值,无需 JS 干预。
屏幕宽度 规格 单格尺寸 棋盘总宽 是否溢出 ≥601px 3×3 100px 308px 否 ≥601px 4×4 100px 412px 否 ≤600px 3×3 70px 218px 否 ≤600px 4×4 70px 292px 否 注意:
width使用calc()而非max-width,确保棋盘始终居中且不拉伸。若用width: 100%,小屏下格子会被压缩变形。5. 进阶技巧:用
data:text/html协议实现一键分享与离线运行5.1 将整个游戏打包为单行 data URL
当需要快速分享给同事或嵌入邮件时,可将 HTML 全文编码为
data:text/html;charset=utf-8,开头的 URL。此 URL 可直接粘贴到浏览器地址栏运行,或保存为.html文件双击打开。关键在于对 HTML 字符串进行encodeURIComponent编码,并替换所有换行符为空格(data URL 不允许换行)。function generateDataUrl() { const htmlStr = document.documentElement.outerHTML .replace(/\n/g, ' ') .replace(/\s+/g, ' '); return `data:text/html;charset=utf-8,${encodeURIComponent(htmlStr)}`; } // 使用示例:复制到剪贴板 document.getElementById('share-btn').addEventListener('click', function() { navigator.clipboard.writeText(generateDataUrl()); alert('Data URL 已复制!粘贴到浏览器地址栏即可运行'); });5.1.1
outerHTML与innerHTML的选择依据
outerHTML包含<html>标签及<!doctype>,确保 data URL 是完整文档;innerHTML仅含<body>内容,会导致<!doctype>丢失,触发怪异模式。实测中,Chrome 90+ 对data:text/html的charset=utf-8解析稳定,Firefox 也完全兼容。5.2 离线运行时的资源兜底策略
游戏完全静态,但若用户禁用 JavaScript,需提供基础可访问性。在
<noscript>中添加说明文本,并用 CSS 隐藏已启用 JS 的页面元素:<noscript> <div style="padding: 20px; text-align: center; color: #666;"> <p>此拼图游戏需要启用 JavaScript 才能运行。</p> <p>请检查浏览器设置,或尝试其他设备。</p> </div> </noscript> <style> body.js-enabled .puzzle-piece { display: inline-block; } body:not(.js-enabled) #puzzle-board { display: none; } </style> <script> document.body.classList.add('js-enabled'); </script>提示:
<noscript>内容必须简洁,避免冗长说明。CSS 中body:not(.js-enabled)选择器确保禁用 JS 时仅显示提示,隐藏所有交互元素,防止用户误操作。5.3 最小化体积优化:内联关键 CSS 与移除未用代码
最终
.html文件应控制在 15KB 以内。实测优化手段包括:
- 移除所有注释(开发时保留,发布前删除)
- 压缩 CSS:合并重复声明,删除空格(如
margin:0替代margin: 0)- 函数名缩短:
initBoard→iB,getCurrentEmptyIndex→gEI- 使用
const替代let(V8 引擎优化更好)经压缩后,3×3 版本 HTML 文件大小为 12.3KB,4×4 版本为 12.7KB,完全满足现代 CDN 的缓存友好性要求。
本文还有配套的精品资源,点击获取