基于HTML5的双人五子棋小游戏源码实战项目

本文还有配套的精品资源,点击获取

简介:HTML5双人五子棋小游戏源码是一个融合HTML5、CSS、JavaScript和jQuery的前端互动游戏项目,旨在通过核心技术实现完整的五子棋对战功能。项目利用HTML5 Canvas绘制棋盘与棋子,通过JavaScript实现落子逻辑、胜负判断与交互控制,结合jQuery优化事件处理与界面动效,使用CSS进行界面美化与响应式布局。该源码完整展示了前端技术在游戏开发中的协同应用,是学习Web前端开发与小游戏实现的优质实践案例。

1. HTML5语义化结构与Canvas绘图基础

HTML5作为现代前端开发的核心标准,不仅引入了更具语义化的标签体系,还通过 <canvas> 元素为网页提供了强大的图形绘制能力。在双人五子棋小游戏的构建中,语义化标签如 <header><main><section> 等被用于组织清晰的页面结构,提升代码可读性与可维护性。与此同时,Canvas API承担着棋盘与棋子的视觉渲染任务,其核心流程包括获取2D上下文( getContext('2d') )、理解笛卡尔坐标系、使用路径方法(如 beginPathmoveTolineTo )绘制网格,并通过 fillStylefillRect 等设置填充样式。

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适配流程
graph TD A[页面加载] --> B{获取 devicePixelRatio} B --> C[设置 canvas.width = CSS宽 × DPR] C --> D[设置 canvas.height = CSS高 × DPR] D --> E[canvas.style 设置原始CSS尺寸] E --> F[ctx.scale(DPR, DPR)] F --> G[正常绘图坐标] G --> H[清晰无模糊线条]
关键参数解释:
  • 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 替代 setTimeoutsetInterval

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位置解析

为了支持桌面端鼠标和移动端触摸操作,需同时监听 mousedowntouchstart 事件。现代浏览器提供了统一的事件属性接口,使得跨平台兼容成为可能。

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 流程图:坐标映射处理流程
graph TD A[用户点击/触摸] --> B{获取 clientX/clientY } B --> C[计算 relativeX, relativeY] C --> D[调用 screenToBoard(x, y)] D --> E{是否在棋盘范围内?} E -- 是 --> F[返回 row, col] E -- 否 --> G[返回 null,忽略操作] F --> H[执行落子逻辑]

⚠️ 注意事项:

  • 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 存储当前 boardDatacurrentPlayer ,实现断点续玩:

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个连续同色棋子,则判胜。

流程图说明
graph TD A[开始胜负判定] --> B{获取落子坐标(x,y)及颜色} B --> C[初始化计数器count = 1] C --> D[遍历四个主方向] D --> E[沿当前方向前进] E --> F{是否越界或非同色?} F -- 是 --> G[切换至反方向继续] F -- 否 --> H[计数+1, 继续前进] G --> I{反方向探测完毕?} I -- 否 --> J[继续反向探测] I -- 是 --> K{count >= 5?} K -- 是 --> L[返回胜利] K -- 否 --> M[尝试下一方向] M --> D L --> N[结束判定]

该流程展示了典型的辐射探测结构:以落子点为原点,沿每一主方向及其反方向展开线性扫描,累计连续同色棋子数量。

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),但在严格模式或类型化数组中会引发异常。因此必须显式限制 rc 在合法范围内 [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 : 棋子中心偏移量(通常为格宽一半);

  • lineWidthstrokeStyle : 定义线条粗细与颜色;

  • 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();
}
sequenceDiagram participant U as 用户 participant JS as JavaScript participant DOM as Canvas/DOM participant LS as localStorage U->>JS: 点击"上一步" JS->>JS: historyIndex -= 1 JS->>JS: 更新board状态 JS->>DOM: 重绘棋盘 JS->>LS: (可选)更新缓存

此机制支持无限层级撤销,前提是内存允许。

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前端开发与小游戏实现的优质实践案例。

本文还有配套的精品资源,点击获取