简介:HTML5双人五子棋小游戏源码是一个融合HTML5、CSS、JavaScript和jQuery的前端互动游戏项目,旨在通过核心技术实现完整的五子棋对战功能。项目利用HTML5 Canvas绘制棋盘与棋子,通过JavaScript实现落子逻辑、胜负判断与交互控制,结合jQuery优化事件处理与界面动效,使用CSS进行界面美化与响应式布局。该源码完整展示了前端技术在游戏开发中的协同应用,是学习Web前端开发与小游戏实现的优质实践案例。
1. HTML5语义化结构与Canvas绘图基础
HTML5作为现代前端开发的核心标准,不仅引入了更具语义化的标签体系,还通过 <canvas> 元素为网页提供了强大的图形绘制能力。在双人五子棋小游戏的构建中,语义化标签如 <header> 、 <main> 、 <section> 等被用于组织清晰的页面结构,提升代码可读性与可维护性。与此同时,Canvas API承担着棋盘与棋子的视觉渲染任务,其核心流程包括获取2D上下文( getContext('2d') )、理解笛卡尔坐标系、使用路径方法(如 beginPath 、 moveTo 、 lineTo )绘制网格,并通过 fillStyle 、 fillRect 等设置填充样式。
javascript
const canvas = document.getElementById('gameBoard');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#f0d9b5';
ctx.fillRect(0, 0, canvas.width, canvas.height);
上述代码初始化画布背景色,是绘图的第一步。Canvas以即时模式(immediate mode)工作,适合频繁更新的像素级操作,相较于SVG的DOM式保留模式,更适用于高频率重绘的游戏场景,但缺乏内置的事件绑定机制,需结合JavaScript手动处理交互。本章将为后续动态渲染打下坚实基础。
2. 基于Canvas的棋盘与棋子动态渲染
在现代Web前端开发中,Canvas元素已经成为实现高性能图形绘制的重要工具。尤其在游戏类应用中,如双人五子棋小游戏,Canvas不仅承担着棋盘、棋子等静态视觉元素的绘制任务,还需支持高频更新和流畅动画效果。本章将围绕"基于Canvas的棋盘与棋子动态渲染"这一核心主题,系统性地探讨如何利用HTML5 Canvas API构建一个结构清晰、响应灵敏且视觉精致的游戏界面。
Canvas的核心优势在于其 像素级控制能力 ,开发者可以通过JavaScript直接操作绘图上下文( CanvasRenderingContext2D ),实现从线条到复杂形状的自由绘制。然而,这种灵活性也带来了性能管理、设备适配和状态维护等方面的挑战。特别是在高分辨率屏幕或低性能移动设备上,若未进行合理优化,极易出现画面模糊、卡顿甚至内存泄漏等问题。
因此,本章的重点不仅是完成基本的绘图功能,更是通过科学的设计策略提升整体渲染效率与用户体验一致性。我们将分三个主要部分展开: 棋盘网格的生成与优化 、 棋子的可视化绘制 以及 动态渲染性能控制机制 。每一部分都将结合实际代码示例、参数说明与逻辑分析,深入剖析关键技术点,并引入表格对比、mermaid流程图和多维度代码解析,确保内容具备足够的技术深度与实践指导价值。
2.1 棋盘网格的生成与优化
五子棋棋盘通常为15×15的标准格网布局,要求横竖线间距均匀、交叉点对齐精确。在Canvas中实现该结构看似简单,但要保证跨设备兼容性和视觉清晰度,则需综合考虑坐标系统、尺寸适配及像素对齐等多个因素。
2.1.1 Canvas坐标系与网格线绘制逻辑
Canvas采用左上角为原点 (0, 0) 的笛卡尔坐标系,x轴向右递增,y轴向下递增。这意味着所有图形的位置都必须以该基准计算。对于棋盘而言,首先需要确定画布总尺寸(如 600×600px )并据此划分出等距的行列。
使用 context.beginPath() 开始路径后,可循环调用 moveTo(x1, y1) 和 lineTo(x2, y2) 方法来绘制每一条水平线和垂直线。最后通过 stroke() 触发实际渲染。
javascript
const canvas = document.getElementById('chessboard');
const ctx = canvas.getContext('2d');
const BOARD_SIZE = 15;
const CELL_SIZE = 40;
// 绘制水平线
for (let i = 0; i <= BOARD_SIZE; i++) {
const y = i * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(BOARD_SIZE * CELL_SIZE, y);
ctx.stroke();
}
// 绘制垂直线
for (let i = 0; i <= BOARD_SIZE; i++) {
const x = i * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, BOARD_SIZE * CELL_SIZE);
ctx.stroke();
}
代码逻辑逐行解读:
- 第1--2行:获取DOM中的canvas元素及其2D渲染上下文。
- 第3--4行:定义棋盘大小(15格)与单个格子的像素宽度。
- 第7--13行:循环绘制水平线。每次设定起始点
(0, y)到终点(600, y),形成一条贯穿画布的直线。 - 第16--22行:同理绘制垂直线。
beginPath()的作用是重置当前路径,避免多次绘制叠加影响性能。
⚠️ 注意:频繁调用
beginPath()和stroke()可能导致性能下降,后续章节将介绍批量绘制优化方案。
2.1.2 网格间距计算与画布尺寸适配策略
为了使棋盘在不同设备上都能完整显示且比例协调,必须动态计算 CELL_SIZE 。理想情况下,应根据容器宽度自动调整格子大小,同时保持整数像素值以防止模糊。
| 设备类型 | 推荐画布宽度 | 单元格建议尺寸 | 行列数 |
|---|---|---|---|
| 手机 | 300--400px | 20--25px | 15×15 |
| 平板 | 500--600px | 30--40px | 15×15 |
| 桌面 | 700--800px | 40--50px | 15×15 |
可通过以下函数实现自适应:
javascript
function resizeCanvas(container) {
const rect = container.getBoundingClientRect();
const size = Math.min(rect.width, rect.height); // 正方形区域
const boardLength = Math.floor(size / 16) * 15; // 预留边距
const cellSize = boardLength / BOARD_SIZE;
canvas.width = boardLength;
canvas.height = boardLength;
return { boardLength, cellSize };
}
参数说明:
getBoundingClientRect()获取容器真实渲染尺寸;Math.min(...)确保棋盘为正方形;Math.floor(size / 16) * 15是一种保守缩放法,预留至少一格边距;- 返回对象可用于后续绘图依赖。
该策略确保无论视口如何变化,棋盘始终居中且不溢出。
2.1.3 高DPI设备下的线条模糊问题与像素对齐解决方案
在Retina屏等高DPI设备上,CSS像素与物理像素并非1:1对应,若直接使用整数坐标仍可能产生抗锯齿导致的"模糊线"。根本原因是Canvas默认按CSS像素渲染,而高倍屏需更高分辨率缓冲区。
解决方法是检测设备像素比( devicePixelRatio ),并放大画布缓冲区,再通过CSS缩放还原显示尺寸:
javascript
function setupHiDPICanvas(canvas, width, height) {
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr); // 缩放绘图上下文
return ctx;
}
流程图:高DPI适配流程
关键参数解释:
devicePixelRatio:设备物理像素与CSS像素的比例,常见值为1(普通屏)、2(Retina)、3(高端手机);scale(dpr, dpr):使得后续所有绘图命令自动乘以DPR,无需手动换算坐标;- 结果:即使绘制
strokeLine(0, 0, 100, 0),也会在物理层面占据整数像素位置,消除亚像素渲染带来的模糊。
此外,还应注意线条宽度设置。例如, ctx.lineWidth = 1 在高DPI下实际占1/CSS像素,可能导致过细。推荐设置为奇数(如1.5、2.5),并在中心绘制时偏移0.5像素以对齐像素边界:
javascript
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#333';
综上所述,通过合理的坐标计算、动态尺寸适配与高DPI补偿机制,可以构建出在各类设备上均表现优异的棋盘网格系统,为后续棋子绘制打下坚实基础。
2.2 棋子的可视化绘制
棋子作为游戏中最核心的交互反馈元素,其外观质量直接影响用户体验。本节将详细阐述如何使用Canvas的绘图API实现美观且高效的棋子渲染。
2.2.1 使用arc方法绘制圆形棋子
Canvas提供了 arc(x, y, radius, startAngle, endAngle, anticlockwise) 方法用于绘制圆弧或完整圆形。五子棋棋子即是以此为基础的实心圆。
javascript
function drawPiece(ctx, row, col, color, cellSize, offset = 0) {
const centerX = col * cellSize + cellSize / 2;
const centerY = row * cellSize + cellSize / 2;
const radius = cellSize * 0.45; // 留出边缘间隙
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
参数说明:
row,col:逻辑上的棋盘坐标;cellSize:每个格子的像素宽度;offset:可用于动画过渡(如下落缓动);radius = cellSize * 0.45:略小于半个格子,避免贴边;fillStyle支持颜色字符串(如'black','#000')或渐变对象。
调用示例:
javascript
drawPiece(ctx, 7, 7, 'black', 40); // 在中心位置画黑子
drawPiece(ctx, 7, 8, 'white', 40); // 相邻画白子
✅ 提示:应在每次落子后仅重绘受影响区域,而非全盘刷新,详见2.3节性能控制。
2.2.2 渐变填充与阴影效果增强视觉质感
为进一步提升视觉层次感,可为棋子添加径向渐变和轻微阴影。
javascript
function createGradient(ctx, x, y, r, innerColor, outerColor) {
const gradient = ctx.createRadialGradient(
x - r * 0.3, y - r * 0.3, 0, // 内光点偏移
x, y, r // 外圈中心与半径
);
gradient.addColorStop(0, innerColor);
gradient.addColorStop(1, outerColor);
return gradient;
}
// 绘制带光泽的黑子
function drawShinyBlackPiece(ctx, row, col, cellSize) {
const cx = col * cellSize + cellSize / 2;
const cy = row * cellSize + cellSize / 2;
const r = cellSize * 0.45;
const gradient = createGradient(ctx, cx, cy, r, '#fff', '#000');
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.fillStyle = gradient;
ctx.fill();
// 添加柔和阴影
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
}
效果分析:
- 径向渐变模拟了光线照射下的立体感;
addColorStop(0, '#fff')在中心加入亮斑,模仿反光;- 阴影参数控制投影强度与方向,避免过度夸张;
- 实际项目中建议封装为样式配置模块,便于统一管理。
2.2.3 黑白棋子状态管理与重绘机制
为支持游戏过程中的动态更新,必须建立棋子数据模型并与视图同步。
设计一个二维数组表示棋盘状态:
javascript
const BOARD_STATE = Array(15).fill().map(() => Array(15).fill(0));
// 0: 空, 1: 黑子, 2: 白子
每当用户落子,更新状态并触发重绘:
javascript
function placePiece(row, col, player) {
if (BOARD_STATE[row][col] !== 0) return false; // 已有棋子
BOARD_STATE[row][col] = player;
redrawPiece(row, col); // 局部重绘
return true;
}
function redrawPiece(row, col) {
const cellSize = 40;
const value = BOARD_STATE[row][col];
const color = value === 1 ? 'black' : value === 2 ? 'white' : null;
if (!color) return;
drawPiece(ctx, row, col, color, cellSize);
}
优化建议:
- 不采用
clearRect(0, 0, w, h)全局清屏,而是只清除特定格子; - 使用"脏区域标记"机制记录待更新坐标,延迟批量处理;
- 支持撤销操作时,保留历史快照以便恢复。
该机制确保了数据与视图的高度一致性,也为后续胜负判断和复盘功能提供支撑。
2.3 动态渲染性能控制
随着游戏进程推进,频繁的绘图操作可能引发性能瓶颈。为此,必须引入高效渲染策略。
2.3.1 requestAnimationFrame在动画中的应用
当需要实现棋子下落动画或胜利特效时,应使用 requestAnimationFrame 替代 setTimeout 或 setInterval 。
javascript
function animateDrop(row, col, targetY, duration = 300) {
const startY = 0;
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3); // 缓入缓出
const currentY = startY + (targetY - startY) * easeProgress;
clearCell(row, col); // 清除旧位置
drawFloatingPiece(row, col, currentY); // 浮动绘制
if (progress < 1) {
requestAnimationFrame(step);
} else {
finalizePiece(row, col); // 落定
}
}
requestAnimationFrame(step);
}
优势说明:
requestAnimationFrame由浏览器统一调度,与刷新率同步(通常60fps);- 自动暂停隐藏标签页中的动画,节省资源;
- 时间精度更高,适合精细动画控制。
2.3.2 避免重复绘制的脏区域检测技术
引入"脏矩形"机制,仅重绘发生变化的部分:
javascript
const dirtyRegions = [];
function markDirty(row, col, cellSize) {
const x = col * cellSize;
const y = row * cellSize;
dirtyRegions.push({ x, y, width: cellSize, height: cellSize });
}
function flushDirty() {
ctx.save();
for (const region of dirtyRegions) {
ctx.clearRect(region.x, region.y, region.width, region.height);
// 重新绘制该区域内所有内容
const r = Math.floor(region.y / cellSize);
const c = Math.floor(region.x / cellSize);
redrawPiece(r, c);
}
dirtyRegions.length = 0; // 清空队列
ctx.restore();
}
性能收益:
- 减少不必要的全屏绘制;
- 特别适用于大型棋盘或多棋子场景;
- 可扩展为区域合并算法(相邻dirty box合并)进一步优化。
2.3.3 多次操作后的画面恢复与缓存机制设计
对于复杂的背景(如木纹纹理棋盘),反复重绘成本高昂。可预先绘制到离屏Canvas并缓存:
javascript
const offscreenCanvas = document.createElement('canvas');
const offCtx = offscreenCanvas.getContext('2d');
function cacheBackground() {
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
drawChessboardGrid(offCtx); // 包括纹理、坐标标记等
}
function restoreBackground() {
ctx.drawImage(offscreenCanvas, 0, 0);
}
应用场景:
- 每次重绘前先恢复背景,再叠加棋子;
- 配合局部重绘,极大降低CPU负载;
- 适合包含装饰性元素(星位、边框)的复杂界面。
综上,通过对棋盘生成、棋子绘制与渲染性能的系统化设计,我们构建了一个兼具视觉美感与运行效率的Canvas五子棋渲染体系。这些技术不仅适用于当前项目,也可广泛迁移至其他基于Canvas的交互式图形应用中。
3. JavaScript驱动的游戏交互与核心逻辑实现
在双人五子棋小游戏的开发中,JavaScript 扮演着"大脑"角色,负责处理用户输入、管理游戏状态、维护数据模型,并协调视图更新。如果说 HTML5 Canvas 提供了视觉呈现的基础,那么 JavaScript 则是让整个游戏"活起来"的关键驱动力。本章将深入探讨如何通过事件系统捕捉玩家操作,设计健壮的状态机控制流程,以及构建高效的数据结构来同步逻辑与界面,确保游戏具备良好的响应性与可扩展性。
3.1 用户点击事件与坐标映射
用户交互是任何游戏体验的核心环节。在基于 Canvas 的五子棋项目中,由于 Canvas 是一个位图像素画布而非 DOM 元素集合,无法像传统按钮那样直接绑定事件到具体图形对象(如某个棋格),因此必须通过事件监听结合坐标换算的方式,实现从屏幕点击位置到棋盘逻辑坐标的精确映射。
3.1.1 鼠标/触屏事件监听与event.offset位置解析
为了支持桌面端鼠标和移动端触摸操作,需同时监听 mousedown 和 touchstart 事件。现代浏览器提供了统一的事件属性接口,使得跨平台兼容成为可能。
javascript
const canvas = document.getElementById('gameCanvas');
// 同时监听鼠标和触摸事件
canvas.addEventListener('mousedown', handleInput);
canvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // 阻止默认滚动行为
handleInput(e.touches[0]); // 取第一个触点
});
function handleInput(event) {
const rect = canvas.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
console.log(`点击位置: (${offsetX}, ${offsetY})`);
}
代码逻辑逐行解读:
- 第 2 行:获取
<canvas>元素引用,用于后续事件绑定。 - 第 4--5 行:为
mousedown添加处理函数;对touchstart使用匿名函数包装以提取touches[0]触点信息。 - 第 7 行:调用
preventDefault()是关键步骤,防止移动设备上因点击引发页面滚动或缩放,影响用户体验。 - 第 8 行:
event.touches[0]获取首个触点坐标,适用于多点触控场景下的单点落子需求。 - 第 12--13 行:使用
getBoundingClientRect()获取画布相对于视口的位置,进而计算出相对于画布左上角的偏移量(offsetX,offsetY)。
该方法的优点在于不依赖外部库即可完成精准定位,且兼容主流浏览器。但需注意不同设备下指针精度差异,例如触控笔与手指点击的有效区域不同。
不同输入方式的事件属性对比表:
| 输入类型 | 事件名称 | 坐标属性 | 是否需要 preventDefault |
|---|---|---|---|
| 鼠标 | mousedown | clientX, clientY | 否 |
| 触摸 | touchstart | touches[0].clientX/Y | 是(防滚动) |
| 指针 | pointerdown | clientX, clientY | 推荐 |
💡 提示 :推荐使用 Pointer Events API(如
pointerdown)作为统一事件模型,可自动合并鼠标、触摸、触控笔等输入源,提升代码一致性。
3.1.2 屏幕坐标到棋盘格点坐标的转换算法
Canvas 绘制的棋盘通常由 15×15 的网格构成,每个格子宽度为 cellSize 像素。要将屏幕坐标 (x, y) 映射为逻辑上的棋盘行列索引 (row, col) ,需进行如下数学变换:
\text{col} = \left\lfloor \frac{x + \frac{\text{cellSize}}{2}}{\text{cellSize}} \right\rfloor \
\text{row} = \left\lfloor \frac{y + \frac{\text{cellSize}}{2}}{\text{cellSize}} \right\rfloor
其中加 cellSize / 2 实现"四舍五入"式吸附,避免边缘误判。
javascript
const BOARD_SIZE = 15;
const cellSize = 40; // 每个格子40px
function screenToBoard(x, y) {
let col = Math.floor((x + cellSize / 2) / cellSize);
let row = Math.floor((y + cellSize / 2) / cellSize);
// 边界检查
if (col < 0 || col >= BOARD_SIZE || row < 0 || row >= BOARD_SIZE) {
return null;
}
return { row, col };
}
参数说明:
-
x, y:来自事件的屏幕坐标(已转换为相对于画布) -
cellSize:预设的每格像素大小,决定分辨率与布局密度 -
返回值:若在有效范围内返回
{row, col},否则返回null
此算法实现了从连续空间到离散格点的量化映射,是连接物理输入与逻辑操作的关键桥梁。
mermaid 流程图:坐标映射处理流程
⚠️ 注意事项:
若
cellSize不整除画布尺寸,可能导致边缘空白或挤压,建议动态计算以适配容器;在高 DPI 设备上应考虑 devicePixelRatio 缩放问题,避免坐标偏差。
3.1.3 格点吸附机制与落子位置校准
理想情况下,玩家点击后棋子应准确落在最近的交叉点上,即使点击略有偏差。这需要引入"吸附机制",即将实际落子渲染位置锁定在网格交点处。
javascript
function renderPiece(ctx, row, col, color) {
const centerX = col * cellSize;
const centerY = row * cellSize;
ctx.beginPath();
ctx.arc(centerX, centerY, 16, 0, Math.PI * 2); // 半径16px
ctx.fillStyle = color === 'black' ? '#000' : '#fff';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.stroke();
}
结合前一步的 screenToBoard 输出结果,即可实现"点击任意位置 → 自动对齐最近格点 → 渲染棋子"。
进一步优化可加入视觉反馈动画,例如先显示半透明预览棋子,确认后再实填:
javascript
canvas.addEventListener('mousemove', (e) => {
if (gameState !== 'playing') return;
const pos = getRelativePosition(e);
const boardPos = screenToBoard(pos.x, pos.y);
if (boardPos) {
drawPreviewPiece(boardPos.row, boardPos.col, currentPlayer);
}
});
这样不仅提升了操作容错率,也增强了交互直观性,尤其适合移动端小屏幕环境。
3.2 游戏状态机设计
复杂的游戏行为可以通过状态机模式清晰建模。五子棋具有明确的生命周期阶段:初始化、进行中、结束。合理设计状态流转机制,有助于解耦业务逻辑、提升代码可维护性。
3.2.1 回合制玩家切换逻辑(黑方与白方交替)
采用有限状态机(FSM)思想,定义当前玩家变量并配合事件触发切换:
javascript
let currentPlayer = 1; // 1=黑, 2=白
function switchPlayer() {
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateUIIndicator(); // 更新界面指示器
}
function placePiece(row, col) {
if (!isValidMove(row, col)) return false;
boardData[row][col] = currentPlayer;
renderPieceOnCanvas(row, col, currentPlayer);
if (checkWin(row, col, currentPlayer)) {
endGame(currentPlayer);
} else {
switchPlayer();
}
}
逻辑分析:
-
currentPlayer作为全局状态标识,直接影响棋子颜色与胜负判定; -
placePiece成功后才调用switchPlayer,保证非法操作不会改变回合; -
使用数字而非字符串表示玩家,便于后续集成 AI 或网络对战时做数值比较。
该设计简洁高效,易于扩展为支持"悔棋"、"暂停"等高级功能。
3.2.2 游戏初始化、进行中、结束三种状态的流转控制
引入显式的状态变量 gameState 来控制整体流程:
javascript
let gameState = 'init'; // 'init', 'playing', 'ended'
function initGame() {
boardData = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
currentPlayer = 1;
clearCanvas();
drawBoardGrid();
gameState = 'playing';
}
function endGame(winner) {
gameState = 'ended';
drawVictoryLine(winnerMoves); // 绘制胜利连线
showModal(`玩家${winner === 1 ? '黑' : '白'}获胜!`);
}
状态流转表格:
| 当前状态 | 触发事件 | 新状态 | 动作说明 |
|---|---|---|---|
| init | 页面加载完成 | playing | 初始化棋盘数组,绘制网格 |
| playing | 成功落子且未获胜 | playing | 切换玩家,继续游戏 |
| playing | 检测到五子连珠 | ended | 显示胜者,禁用后续输入 |
| ended | 点击"重新开始" | playing | 重置数据,重新初始化 |
这种状态驱动的设计避免了复杂的条件嵌套,使程序行为更加可控。
3.2.3 不可重复落子与禁手规则预留接口设计
为防止重复落子,在 isValidMove 中加入检测:
javascript
function isValidMove(row, col) {
return gameState === 'playing' &&
row >= 0 && row < BOARD_SIZE &&
col >= 0 && col < BOARD_SIZE &&
boardData[row][col] === 0; // 空位才能下
}
此外,为未来扩展"禁手"规则(如三三禁手、长连禁手等),可预留钩子函数:
javascript
function isForbiddenMove(row, col, player) {
if (player !== 1) return false; // 目前仅黑方受限
// TODO: 实现禁手检测算法
return false;
}
// 调用时机:
if (isForbiddenMove(row, col, currentPlayer)) {
alert("此为禁手位置,禁止落子!");
return;
}
此设计遵循开放封闭原则------现有功能稳定运行的同时,允许在未来无缝接入更复杂的规则体系。
3.3 数据模型与视图同步
良好的 MVC(Model-View-Controller)分离是构建可维护前端应用的关键。在五子棋项目中,我们以二维数组为核心数据模型,通过事件驱动机制实现与 Canvas 视图的实时同步。
3.3.1 二维数组表示棋盘状态(0:空, 1:黑, 2:白)
javascript
let boardData = Array(15).fill(null).map(() => Array(15).fill(0));
// 示例:初始状态
/*
[
[0,0,0,...],
[0,0,0,...],
...
]
*/
优势分析:
-
内存紧凑,访问速度快(O(1) 时间复杂度);
-
便于遍历检测胜负(行列循环天然匹配);
-
序列化简单,利于存储与传输。
该结构虽简单,却是所有高级功能(如 AI 评估、复盘回放)的基础。
3.3.2 落子后数据更新与Canvas视图刷新联动
每次合法落子都应触发"数据变更 → 视图重绘"链条:
javascript
function makeMove(row, col) {
if (!isValidMove(row, col)) return;
boardData[row][col] = currentPlayer;
// 视图更新
const color = currentPlayer === 1 ? 'black' : 'white';
renderPiece(ctx, row, col, color);
// 胜负检测
if (checkWin(row, col)) {
gameState = 'ended';
highlightWinningStones(winPath);
} else {
switchPlayer();
}
}
🔁 双向同步策略建议:
所有落子必须先改数据再刷视图,禁止绕过数据直接绘图;
提供
redrawBoardFromData()方法用于异常恢复或主题切换时的整体刷新。
3.3.3 中途退出与重新加载时的状态一致性保障
利用 localStorage 存储当前 boardData 与 currentPlayer ,实现断点续玩:
javascript
window.addEventListener('beforeunload', () => {
localStorage.setItem('gobang_state', JSON.stringify({
board: boardData,
player: currentPlayer,
timestamp: Date.now()
}));
});
// 页面加载时恢复
window.addEventListener('load', () => {
const saved = localStorage.getItem('gobang_state');
if (saved) {
const state = JSON.parse(saved);
boardData = state.board;
currentPlayer = state.player;
redrawBoardFromData(); // 根据数据重建画面
gameState = 'playing';
} else {
initGame();
}
});
此机制确保用户意外关闭页面后仍能恢复进度,极大提升产品体验完整性。
数据持久化字段说明表:
| 字段名 | 类型 | 用途 |
|---|---|---|
| board | 数组 | 棋盘状态矩阵 |
| player | number | 当前应下子的玩家编号 |
| timestamp | number | 保存时间戳,可用于自动清理旧数据 |
综上所述,JavaScript 不仅承担了事件响应与逻辑判断的任务,更是维系数据、状态与视图之间一致性的中枢系统。通过科学的架构设计,即使是轻量级的前端游戏也能具备工业级的稳定性与扩展潜力。
4. 五子连珠胜负判定算法深度实现
在双人对弈类游戏的开发中,胜负判定是决定用户体验完整性和公平性的核心逻辑模块。对于五子棋这类规则明确但状态空间庞大的策略性游戏,如何高效、准确地判断是否出现"五子连珠"成为系统设计的关键挑战之一。本章将围绕 从落子后状态出发,精准识别任意方向上的连续五个同色棋子 这一目标,深入剖析判定算法的设计原理与优化路径。通过构建可扩展、低延迟的检测机制,确保每一次落子都能即时反馈结果,同时为未来支持禁手规则或AI评估函数预留接口。
胜负判定不仅是技术实现问题,更是性能与正确性之间的权衡艺术。尤其是在基于Canvas的动态渲染环境中,频繁调用高复杂度算法可能导致帧率下降,影响交互流畅性。因此,必须采用 以局部检测为核心、避免全局扫描 的策略,在保证逻辑严谨的前提下最大化运行效率。以下章节将从基础思路入手,逐步推进至高性能优化方案,并结合视觉反馈机制完成闭环体验设计。
4.1 连续五子检测的基本思路
胜负判定的第一步在于建立清晰的数学模型:当某一玩家在水平、垂直或两个对角线方向上形成连续五个相同颜色的棋子时,即判定胜利。由于每次仅新增一枚棋子,无需遍历整个棋盘,只需以该落子点为中心,向八个方向进行辐射式探测即可完成判断。
这种" 中心扩散法 "显著降低了计算量,其本质是一种剪枝后的广度优先搜索(BFS)变体。相比暴力枚举所有可能连线的方式,它将时间复杂度从 O(n\^2) 下降至 O(1) 级别(固定探测范围),非常适合实时交互场景。
4.1.1 八方向扫描法(横向、纵向、两个对角线)
为了覆盖所有可能的五子连线方向,需定义四组互为反向的方向向量:
| 方向 | X增量 (dx) | Y增量 (dy) | 描述 |
|---|---|---|---|
| 水平 | +1, -1 | 0, 0 | 左右延伸 |
| 垂直 | 0, 0 | +1, -1 | 上下延伸 |
| 主对角线 | +1, -1 | +1, -1 | 从左上到右下 |
| 副对角线 | +1, -1 | -1, +1 | 从右上到左下 |
实际编程中可简化为四个方向单位向量,每个方向与其反方向组合构成一条直线探测路径。
javascript
const DIRECTIONS = [
[1, 0], // 水平向右
[0, 1], // 垂直向下
[1, 1], // 主对角线右下
[1, -1] // 副对角线右上
];
上述数组仅保存正方向向量,反方向可通过取负值得到。每条线上最多向前和向后各延伸4格(共9格),若其中有5个连续同色棋子,则判胜。
流程图说明
该流程展示了典型的辐射探测结构:以落子点为原点,沿每一主方向及其反方向展开线性扫描,累计连续同色棋子数量。
4.1.2 从落子点出发的辐射检测策略
具体实现时,采用双重循环结构:外层遍历四个方向向量,内层分别沿正负两个方向延伸探测。关键代码如下:
javascript
function checkWin(board, row, col, player) {
const directions = [[1,0], [0,1], [1,1], [1,-1]];
for (let [dx, dy] of directions) {
let count = 1; // 包含自身
// 正方向探测
for (let i = 1; i < 5; i++) {
const r = row + dx * i;
const c = col + dy * i;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || board[r][c] !== player) break;
count++;
}
// 反方向探测
for (let i = 1; i < 5; i++) {
const r = row - dx * i;
const c = col - dy * i;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || board[r][c] !== player) break;
count++;
}
if (count >= 5) return true;
}
return false;
}
代码逻辑逐行解析:
- 第2行 :定义四个基本方向向量,代表四种独立的连线可能性。
- 第4行 :外层循环遍历每个方向。
- 第6行 :初始化计数器为1,因为当前落子本身计入连续序列。
- 第9--14行 :沿正方向(
dx,dy)逐格探测,边界检查使用硬编码棋盘尺寸(15×15),超出则跳出。 - 第17--22行 :沿反方向(
-dx,-dy)探测,合并形成完整直线。 - 第24行 :只要任一方向达成5子及以上,立即返回胜利标志。
此方法优点在于结构清晰、易于调试,且不会遗漏任何潜在连线。然而也存在重复访问邻近格子的风险,尤其在密集区域多次触发判定时可能产生冗余比较。
4.1.3 边界越界判断与循环终止条件设定
边界处理是此类网格探测中最易出错的部分。JavaScript数组虽允许负索引访问(返回undefined),但在严格模式或类型化数组中会引发异常。因此必须显式限制 r 和 c 在合法范围内 [0, BOARD_SIZE) 。
此外,循环终止条件的设计直接影响性能与准确性:
- 使用
i < 5而非i <= 4提高可读性; - 条件判断顺序至关重要:先检查坐标合法性,再取值比对,防止越界读取;
- 一旦遇到空位或异色棋子即中断,避免无效遍历。
例如:
javascript
if (r < 0 || r >= 15 || c < 0 || c >= 15 || board[r][c] !== player) break;
该语句利用短路求值特性,只有前三个边界条件均通过后才执行 board[r][c] 访问,有效防止运行时错误。
进一步封装时可提取常量:
javascript
const BOARD_SIZE = 15;
if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || board[r][c] !== player) break;
提升代码可维护性,便于后续调整棋盘规模。
4.2 高效判定算法优化
尽管基础辐射检测已具备实用性,但在高频操作(如AI模拟、复盘快进)或多设备并发环境下仍需进一步优化。本节聚焦于减少不必要的内存访问、提升缓存命中率,并探讨位运算等底层加速手段的应用前景。
4.2.1 减少冗余检查:仅检测受影响区域
传统做法是在每次落子后立即调用胜负判定函数。然而,若前一步已确认无胜利状态,且新落子远离历史热点区域,则可跳过部分方向检测。
一种改进策略是引入" 影响域标记 "机制:记录最近几次落子形成的潜在连线区域,仅当新落子进入该区域时才激活全量检测。否则仅做快速边界排除。
另一种更激进的方法是 增量更新连通分量 ------维护每个棋子在四个方向上的连续长度。例如:
javascript
// 扩展数据结构
class ChessPiece {
constructor(player) {
this.player = player;
this.lengths = { hor: 1, ver: 1, dia1: 1, dia2: 1 }; // 各方向连续长度
}
}
每当放置新棋子 (r,c) ,只需查看其前后邻居的 lengths 值并合并:
javascript
// 示例:更新水平方向连续长度
function updateHorizontal(board, r, c, player) {
let leftLen = (c > 0 && board[r][c-1]?.player === player) ? board[r][c-1].lengths.hor : 0;
let rightLen = (c < 14 && board[r][c+1]?.player === player) ? board[r][c+1].lengths.hor : 0;
const total = leftLen + 1 + rightLen;
board[r][c].lengths.hor = total;
// 更新左右端点(若存在)
if (leftLen) board[r][c - leftLen].lengths.hor = total;
if (rightLen) board[r][c + rightLen].lengths.hor = total;
return total >= 5;
}
此方法将判定时间压缩至接近 O(1) ,但增加了数据结构复杂度和同步成本,适合用于需要毫秒级响应的AI推理引擎。
4.2.2 利用位运算加速状态比对(可扩展性讨论)
在更高阶的实现中,可将整行/列/对角线的状态编码为位图(bitboard),利用位运算实现批量匹配。例如,用一个64位整数表示一行15个格子的状态(每位表示黑/白/空)。
假设我们为黑方维护一个位图 blackLine ,其中第 i 位为1表示该位置有黑子:
javascript
// 示例:检测某行是否有五个连续黑子
function hasFiveInRow(bitPattern) {
const mask = 0b11111; // 五个连续1
for (let shift = 0; shift <= 10; shift++) {
if ((bitPattern >> shift) & mask === mask) return true;
}
return false;
}
虽然JavaScript的Number为双精度浮点型,不完全适合位运算,但可通过 BigInt 支持更大位宽:
javascript
const BLACK_BOARD = 0x1F000n; // 表示第12~16位为黑子
const FIVE_MASK = 0x1Fn;
if ((BLACK_BOARD >> 11n) & FIVE_MASK === FIVE_MASK) {
console.log("Found five in a row!");
}
这种方式特别适用于GPU并行计算或WebAssembly集成场景,为未来升级提供良好扩展性。
4.2.3 时间复杂度分析与最坏情况应对
原始辐射检测的时间复杂度为:
T = 4 \times (4 + 4) = 32 \text{次访问}
即每个方向最多探测8个额外格子(前后各4),总共不超过32次数组访问,属于常数时间 O(1) 。
相比之下,暴力扫描所有起始点和方向的时间复杂度为:
O(n^2 \cdot d) \approx 15^2 \times 4 = 900
显然不可接受。
而在极端情况下(如满盘且最后一子获胜),仍需执行全部探测。但由于现代CPU缓存友好性及JS引擎优化,32次内存访问几乎无感知延迟。
| 方法 | 时间复杂度 | 内存开销 | 适用场景 |
|---|---|---|---|
| 辐射检测 | O(1) | 低 | 实时交互 |
| 位图匹配 | O(k) , k≤11 | 中 | AI评估 |
| 全局扫描 | O(n\^2) | 低 | 教学演示 |
推荐在前端交互层使用辐射检测,而在后台AI服务中结合位图进行局面评分。
4.3 胜负结果反馈与游戏终止处理
算法正确性只是胜负判定的一半,另一半在于用户体验的完整性。一旦检测到胜利,应立即冻结操作、可视化标记连线,并引导用户重启游戏。
4.3.1 Canvas上绘制胜利标记线特效
利用Canvas上下文在棋盘上绘制一条高亮连线,直观指示胜者路径。关键在于确定起点与终点坐标:
javascript
function drawVictoryLine(ctx, startRow, startCol, endRow, endCol) {
const GRID_SIZE = 40;
const OFFSET = 20;
ctx.beginPath();
ctx.moveTo(startCol * GRID_SIZE + OFFSET, startRow * GRID_SIZE + OFFSET);
ctx.lineTo(endCol * GRID_SIZE + OFFSET, endRow * GRID_SIZE + OFFSET);
ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(255, 215, 0, 0.8)';
ctx.lineCap = 'round';
ctx.stroke();
// 添加闪光动画(可选)
ctx.shadowColor = 'gold';
ctx.shadowBlur = 10;
ctx.stroke();
}
参数说明:
-
GRID_SIZE: 每格像素宽度; -
OFFSET: 棋子中心偏移量(通常为格宽一半); -
lineWidth和strokeStyle: 定义线条粗细与颜色; -
shadowBlur: 创建发光效果增强视觉冲击。
调用时机应在 checkWin 返回 true 后,传入探测得到的最长连线两端点。
4.3.2 弹窗提示胜者信息并阻止后续操作
通过DOM操作显示模态框,阻断新的鼠标事件:
javascript
function showWinner(player) {
const modal = document.getElementById('winnerModal');
const message = document.getElementById('winnerMessage');
message.textContent = `${player === 1 ? '黑方' : '白方'} 获胜!`;
modal.style.display = 'flex';
// 解除事件监听
canvas.removeEventListener('click', handleBoardClick);
}
HTML结构示例:
html
<div id="winnerModal" class="modal">
<div class="modal-content">
<p id="winnerMessage"></p>
<button onclick="resetGame()">重新开始</button>
</div>
</div>
CSS建议使用 flex 居中显示,背景遮罩防止底层交互。
4.3.3 提供"重新开始"按钮触发全局重置逻辑
点击"重新开始"应重置以下内容:
javascript
function resetGame() {
// 清空棋盘数据
board = Array(15).fill().map(() => Array(15).fill(0));
// 重置当前玩家
currentPlayer = 1;
// 隐藏胜利提示
document.getElementById('winnerModal').style.display = 'none';
// 重新绑定事件
canvas.addEventListener('click', handleBoardClick);
// 重绘空白棋盘
renderBoard(ctx);
}
该函数确保数据模型、视图、事件三者同步归零,形成完整的状态闭环。
综上所述,胜负判定不仅依赖精确的算法设计,还需融合视觉呈现与交互控制,最终实现技术与体验的统一。
5. 本地存储与响应式体验的完整闭环构建
5.1 基于localStorage的棋局持久化
在现代Web应用中,用户体验的一个重要维度是"状态延续性"。对于像五子棋这样的交互式游戏,用户可能因刷新页面、意外关闭浏览器或切换设备而丢失当前对局进度。为解决这一问题, localStorage 提供了一种简单但高效的客户端数据持久化方案。
5.1.1 序列化当前棋盘状态为JSON字符串
我们使用一个二维数组 board 来表示棋盘状态(0:空, 1:黑, 2:白),该结构天然适合通过 JSON.stringify() 方法进行序列化:
javascript
function saveGameState(board, currentPlayer) {
const gameState = {
board: board,
currentPlayer: currentPlayer,
lastMoveTime: new Date().toISOString()
};
localStorage.setItem('gobangGame', JSON.stringify(gameState));
}
上述代码将当前棋盘、轮到哪位玩家以及最后操作时间一并保存。 JSON.stringify 能够正确处理嵌套数组和基本类型,确保结构完整性。
5.1.2 自动保存最近一次有效局面
为了实现自动保存,我们在每次合法落子后调用保存函数:
javascript
function makeMove(x, y) {
if (isValidMove(x, y)) {
board[x][y] = currentPlayer;
renderStone(x, y); // 渲染棋子
checkWin(x, y); // 判定胜负
saveGameState(board, currentPlayer === 1 ? 2 : 1); // 切换并保存
}
}
此外,可设置防抖机制防止高频写入:
javascript
let saveTimeout;
function debouncedSave(board, nextPlayer) {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveGameState(board, nextPlayer);
}, 300);
}
5.1.3 页面加载时自动恢复未完成棋局
在页面初始化阶段,尝试从 localStorage 恢复数据:
javascript
function loadGameState() {
const saved = localStorage.getItem('gobangGame');
if (saved) {
const { board, currentPlayer, lastMoveTime } = JSON.parse(saved);
// 验证数据有效性
if (Array.isArray(board) && board.length === 15) {
this.board = board;
this.currentPlayer = currentPlayer;
console.log(`已恢复 ${new Date(lastMoveTime).toLocaleString()} 的棋局`);
redrawBoard(); // 重绘整个棋盘
return true;
}
}
return false; // 无有效记录
}
| 数据项 | 类型 | 说明 |
|---|---|---|
| board | 数组[15][15] | 棋盘状态矩阵 |
| currentPlayer | number (1/2) | 下一步执子方 |
| lastMoveTime | string (ISO8601) | 最后操作时间戳 |
| 存储键名 | 'gobangGame' | 固定标识符 |
| 序列化方式 | JSON.stringify | 标准化格式 |
| 容量限制 | ~5MB | 浏览器上限 |
| 同步性 | 同源共享 | 多标签页可见 |
| 过期策略 | 手动清除 | 不自动失效 |
注意:
localStorage是同步阻塞操作,不宜频繁调用;同时需考虑隐私模式下的异常处理。
5.2 复盘功能的设计与实现
复盘功能提升了游戏的策略分析价值,允许玩家回顾每一步的决策路径。
5.2.1 记录每一步的操作时间戳与坐标信息
引入独立的 moveHistory 数组来追踪所有有效动作:
javascript
const moveHistory = [];
function recordMove(x, y, player) {
moveHistory.push({
x, y, player,
timestamp: performance.now()
});
}
每个动作包含位置、角色和精确到毫秒的时间点,便于后续回放控制。
5.2.2 实现"上一步"、"下一步"控制按钮
HTML 控件示例:
html
<div class="controls">
<button id="btnUndo">上一步</button>
<button id="btnRedo">下一步</button>
</div>
绑定事件逻辑:
javascript
document.getElementById('btnUndo').addEventListener('click', undoLastMove);
document.getElementById('btnRedo').addEventListener('click', redoNextMove);
5.2.3 步骤回退时的数据与视图同步机制
关键在于维护历史指针,并重建局部状态:
javascript
let historyIndex = -1;
function undoLastMove() {
if (historyIndex < 0) return;
const move = moveHistory[historyIndex];
board[move.x][move.y] = 0; // 清除棋子
historyIndex--;
redrawBoard(); // 全量重绘(或局部擦除)
updateCurrentPlayer(move.player); // 回切回合
}
function replayToStep(targetIndex) {
// 从初始状态开始逐步重演至目标步数
resetBoard();
for (let i = 0; i <= targetIndex; i++) {
const m = moveHistory[i];
board[m.x][m.y] = m.player;
}
redrawBoard();
}
此机制支持无限层级撤销,前提是内存允许。
5.3 响应式布局与多端适配
随着移动设备普及,响应式设计已成为标配。
5.3.1 使用CSS媒体查询适应不同屏幕尺寸
css
@media (max-width: 768px) {
canvas {
width: 90vw;
height: 90vw;
}
.controls button {
font-size: 16px;
padding: 10px;
}
}
@media (min-width: 769px) {
canvas {
width: 600px;
height: 600px;
}
}
动态调整画布尺寸比例,保证在小屏设备上有足够操作空间。
5.3.2 视口meta标签配置与缩放控制
必须在 <head> 中声明:
html
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
禁用缩放避免手势冲突,提升触摸精准度。
5.3.3 移动端触摸事件兼容性处理与交互优化
除了 mousedown ,还需监听 touchstart :
javascript
canvas.addEventListener('touchstart', handleTouch, false);
function handleTouch(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
const x = Math.round((touch.clientX - rect.left) / GRID_SIZE);
const y = Math.round((touch.clientY - rect.top) / GRID_SIZE);
makeMove(x, y);
}
增加触控反馈样式:
css
canvas {
touch-action: manipulation;
cursor: pointer;
}
结合 pointer-event 可进一步统一输入模型。
简介:HTML5双人五子棋小游戏源码是一个融合HTML5、CSS、JavaScript和jQuery的前端互动游戏项目,旨在通过核心技术实现完整的五子棋对战功能。项目利用HTML5 Canvas绘制棋盘与棋子,通过JavaScript实现落子逻辑、胜负判断与交互控制,结合jQuery优化事件处理与界面动效,使用CSS进行界面美化与响应式布局。该源码完整展示了前端技术在游戏开发中的协同应用,是学习Web前端开发与小游戏实现的优质实践案例。
