
简介这份微信小游戏项目源码以五子棋对战为核心面向正在准备期末大作业、课程设计或小程序入门实战的学生与开发者难度适中适合作为可运行、可讲解的完整案例参考。压缩包共229个文件约1.16MB其中58个js文件承担棋盘逻辑、玩家对战与人机对战等核心功能55个wxml与55个wxss负责页面结构与样式58个json用于页面与项目配置另含少量图片与忽略文件整体结构贴近真实小程序工程。资源已经本地编译验证可运行评审分达到98分内容经助教老师审定能较好满足学习与作业提交需求。目前已有132人学习关注。读者可据此梳理小程序的目录组织、页面跳转、棋盘绘制与胜负判定思路并在此基础上做界面美化或功能扩展省去从零搭建框架的时间。1. 五子棋微信小游戏源码从棋盘渲染到胜负判定的完整落地期末大作业和毕业设计最怕的不是没思路而是思路有了却拼不出一个能跑、能演示、能讲清楚技术点的完整工程。这份五子棋微信小游戏小程序项目源码解决的正是这个问题——它把原生小游戏开发里最核心的几块拼图都摆在了明面上Canvas 棋盘绘制、触摸落子交互、基于方向向量的胜负判定、以及微信小游戏特有的生命周期与分包结构。适合正在找微信小游戏代码参考的在校生也适合想从 Web 前端切到小程序游戏开发的从业者。它不依赖 Unity 或任何重型引擎纯原生 JavaScript 加 Canvas 2D 接口代码量可控改起来不费劲。下面按「先看懂结构、再动手改、最后避坑」的顺序拆一遍每一步都落到能直接抄的代码和参数上。2. 工程结构与 Canvas 棋盘渲染15×15 网格怎么画才不糊拿到一份小程序源码第一件事不是急着跑而是先看目录结构判断它是原生小游戏项目还是用 uni-app、Taro 这类框架套出来的。这份源码是原生小游戏结构根目录下有game.js、game.json、project.config.json三个关键文件外加js/目录存放逻辑模块。game.json里配置了设备方向为竖屏、网络超时等基础项project.config.json里则记录了 appid 占位和编译设置。理解这个结构后面改代码才不会把文件放错地方。2.1 原生小游戏与小程序项目的目录差异很多人第一次接触微信小游戏会把它和普通小程序搞混。普通小程序的入口是app.js加若干pages/页面而小游戏没有页面路由概念入口固定是根目录的game.js全局只有一个 Canvas 画布。这份五子棋源码的game.js里做了三件事获取 Canvas 上下文、初始化棋盘数据、注册触摸事件。如果你拿到的是一份普通小程序源码想改成小游戏页面结构那套Page({})是跑不起来的必须换成wx.createCanvas()这套 API。常见做法是先用wx.getSystemInfoSync()拿到屏幕宽高再按比例算出棋盘边长这样不同机型上棋盘不会变形。// game.js 入口初始化画布与棋盘 const sysInfo wx.getSystemInfoSync(); const canvas wx.createCanvas(); const ctx canvas.getContext(2d); // 棋盘按屏幕宽度留出边距保证竖屏下完整显示 const BOARD_SIZE 15; // 15路棋盘 const MARGIN sysInfo.windowWidth * 0.06; const CELL (sysInfo.windowWidth - MARGIN * 2) / (BOARD_SIZE - 1); canvas.width sysInfo.windowWidth; canvas.height sysInfo.windowWidth; // 正方形棋盘区域这段代码的逻辑很直白BOARD_SIZE决定棋盘路数标准五子棋是 15×15交叉点落子而非格子内落子所以计算CELL时除的是BOARD_SIZE - 1。MARGIN取屏宽的 6% 是经验值太小棋盘贴边不好点太大棋盘被压缩。canvas.height直接等于windowWidth保证棋盘区域是正方形后续所有坐标换算都基于这个正方形做省去宽高比换算的麻烦。2.2 棋盘线与星位的绘制参数棋盘绘制分两层底层是木色背景加网格线上层是棋子。网格线用ctx.beginPath()配合moveTo、lineTo逐条画颜色一般取#8B7355这类深木色线宽 1 到 2 像素。星位是五子棋棋盘上的五个参考点位置在 (3,3)、(3,11)、(11,3)、(11,11) 和 (7,7)画成实心小圆。这里有个容易翻车的地方线宽如果设成 1在高分屏上会发虚因为 Canvas 的 1 像素线默认跨在坐标中间。解决办法是把坐标偏移 0.5 像素或者线宽直接给 2。// 绘制棋盘网格与星位 function drawBoard() { ctx.fillStyle #DEB887; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle #8B7355; ctx.lineWidth 2; for (let i 0; i BOARD_SIZE; i) { // 竖线 ctx.beginPath(); ctx.moveTo(MARGIN i * CELL, MARGIN); ctx.lineTo(MARGIN i * CELL, MARGIN (BOARD_SIZE - 1) * CELL); ctx.stroke(); // 横线 ctx.beginPath(); ctx.moveTo(MARGIN, MARGIN i * CELL); ctx.lineTo(MARGIN (BOARD_SIZE - 1) * CELL, MARGIN i * CELL); ctx.stroke(); } // 五个星位 const stars [[3,3],[3,11],[11,3],[11,11],[7,7]]; ctx.fillStyle #8B7355; stars.forEach(([x, y]) { ctx.beginPath(); ctx.arc(MARGIN x * CELL, MARGIN y * CELL, 4, 0, Math.PI * 2); ctx.fill(); }); }drawBoard每次重绘都会清空整个画布再画这是小游戏里最稳妥的做法不要试图只擦除局部容易留下残影。星位半径取 4 像素是视觉上比较舒服的值太小看不见太大盖住交叉点。棋子绘制时半径一般取CELL * 0.4留出一点间隙黑白子用#000和#FFF加一圈深色描边区分。这些参数不是死的但改动时要注意棋子半径不能超过CELL / 2否则相邻棋子会重叠。3. 落子交互与胜负判定方向向量扫描的四个边界坑棋盘画出来只是壳真正决定这份源码能不能当大作业交上去的是落子逻辑和胜负判定写得对不对。五子棋的胜负判定看着简单——连成五个就赢——但实际写起来边界条件能坑掉一大半人。这份源码用的是方向向量扫描法比全盘遍历高效得多也是面试和答辩时能讲清楚的方案。3.1 触摸坐标到棋盘索引的换算用户在屏幕上点一下拿到的是像素坐标(clientX, clientY)需要换算成棋盘的交叉点索引(col, row)。换算公式是col Math.round((clientX - MARGIN) / CELL)row同理。这里必须用Math.round而不是Math.floor因为落子点是交叉点取最近的那个。换算完还要做边界检查col和row都得落在 0 到 14 之间否则就是点到棋盘外面了。另外要判断该位置是否已有棋子有子就不能重复落。// 触摸事件坐标换算 合法性校验 wx.onTouchStart((e) { const touch e.touches[0]; const col Math.round((touch.clientX - MARGIN) / CELL); const row Math.round((touch.clientY - MARGIN) / CELL); // 边界检查超出棋盘直接忽略 if (col 0 || col BOARD_SIZE || row 0 || row BOARD_SIZE) return; // 重复落子检查 if (board[row][col] ! 0) return; board[row][col] currentPlayer; drawPiece(col, row, currentPlayer); if (checkWin(col, row, currentPlayer)) { wx.showToast({ title: currentPlayer 1 ? 黑棋胜 : 白棋胜 }); gameOver true; return; } currentPlayer currentPlayer 1 ? 2 : 1; // 切换执子方 });board是一个 15×15 的二维数组0 表示空位1 表示黑棋2 表示白棋。currentPlayer记录当前该谁下。checkWin传入刚落子的坐标和颜色只检查经过这个点的四条线不需要全盘扫描。wx.showToast是微信小游戏的原生提示比自绘弹窗省事但要注意它默认 1.5 秒后消失如果要做「再来一局」按钮得用wx.showModal或者自己画。3.2 四方向扫描的胜负判定实现胜负判定的核心思路以刚落下的棋子为中心沿水平、垂直、主对角线、副对角线四个方向分别向正负两侧统计同色连续棋子数任意方向达到或超过 5 个即获胜。这里有个细节统计时要把当前棋子本身算进去所以初始计数为 1。四个方向的增量向量分别是(0,1)、(1,0)、(1,1)、(1,-1)注意副对角线的行增量是 1、列增量是 -1写反了就会漏判。// 四方向扫描判定胜负 function checkWin(col, row, player) { const dirs [[0,1],[1,0],[1,1],[1,-1]]; // 横、竖、主对角、副对角 for (const [dx, dy] of dirs) { let count 1; // 正方向延伸 for (let i 1; i 5; i) { const nc col dx * i, nr row dy * i; if (nc 0 || nc BOARD_SIZE || nr 0 || nr BOARD_SIZE) break; if (board[nr][nc] ! player) break; count; } // 负方向延伸 for (let i 1; i 5; i) { const nc col - dx * i, nr row - dy * i; if (nc 0 || nc BOARD_SIZE || nr 0 || nr BOARD_SIZE) break; if (board[nr][nc] ! player) break; count; } if (count 5) return true; } return false; }这段代码里i 5的循环上限值得说一下理论上一条线最多连 5 个就赢了但实际棋盘上可能出现 6 个连子比如长连循环到 5 就够判定胜负了不需要继续数。边界检查nc 0 || nc BOARD_SIZE必须放在取board[nr][nc]之前否则数组越界会直接报错这是新手最容易踩的坑。另外board的索引是board[row][col]还是board[col][row]要统一源码里用的是前者改的时候别搞混。3.3 棋盘状态与重开一局的复位逻辑一局结束后要能重开否则演示到一半就卡死了。复位逻辑要做三件事把board数组全部清零、把currentPlayer重置为黑棋、把gameOver标志置回 false然后重绘棋盘。这里有个性能上的小坑如果每次重开都重新new Array频繁操作会有内存抖动常见做法是复用同一个数组用双重循环把每个元素赋 0。// 重开一局复位状态并重绘 function restartGame() { for (let r 0; r BOARD_SIZE; r) { for (let c 0; c BOARD_SIZE; c) { board[r][c] 0; } } currentPlayer 1; gameOver false; drawBoard(); // 重绘空棋盘 }drawBoard里已经包含了清空画布和重画网格的逻辑所以复位时直接调它就行不用额外clearRect。如果要做「悔棋」功能思路是用一个栈记录每一步的(col, row, player)悔棋时弹出栈顶、把对应位置置 0、切换执子方、重绘。这个功能源码里没带但加上去不难答辩时也是个加分项。4. 常见问题与排查从白屏到判定失效的五条血泪记录源码跑不起来或者跑起来不对原因往往集中在几个固定位置。下面这五条是我在帮人调这类五子棋小游戏时反复遇到的每条按「现象 → 原因 → 解决」写清楚照着排查基本能覆盖九成问题。4.1 编译后白屏控制台无报错现象是开发者工具里模拟器一片白Console 面板干净得可疑。原因通常是game.json里deviceOrientation配成了landscape但代码里按竖屏算的坐标画布内容画到了可视区域外面。解决方法是把game.json里的方向改回portrait或者确认project.config.json里的编译模式选的是「小游戏」而不是「小程序」。还有一种可能是game.js里wx.createCanvas()调用时机太早放在wx.onShow回调里更稳。4.2 点击落子位置偏移现象是点交叉点棋子却画到了旁边。原因是坐标换算时用了Math.floor而不是Math.round或者MARGIN和CELL的计算基准不一致——比如MARGIN用的是windowWidthCELL却用了screenWidth。解决方法是统一用wx.getSystemInfoSync().windowWidth作为基准换算时坚持Math.round并在drawPiece里用同一套MARGIN col * CELL公式反推绘制坐标保证落子和绘制用的是同一个坐标系。4.3 胜负判定在边缘位置失效现象是在棋盘最边上连成五个程序却没判赢。原因是checkWin里的边界检查写成了nc 0 || nc BOARD_SIZE把索引 0 也排除掉了。正确写法是nc 0 || nc BOARD_SIZE索引 0 是合法位置。这个坑很隐蔽因为棋盘中间落子测试时一切正常只有贴边才暴露。改完后建议专门在四个角和四条边上各测一次五连。4.4 连续落子后画面卡顿现象是下了十几手之后每次落子都有明显延迟。原因是每次落子都调用了drawBoard全量重绘包括重画所有网格线和星位而网格线是不变的。优化思路是把棋盘底图先画到一个离屏 Canvas 上缓存起来落子时只把底图drawImage过来再画棋子。微信小游戏支持wx.createCanvas()创建离屏画布这个优化能把重绘耗时降一半以上。4.5 真机上触摸无响应现象是开发者工具里点得好好的传到手机上点不动。原因是触摸事件绑定在了wx.onTouchStart但小游戏的触摸事件需要确保 Canvas 是顶层且没有被其他元素遮挡。另外部分安卓机型上e.touches[0]的clientX需要除以pixelRatio才能和 Canvas 坐标对齐。解决方法是先打印e.touches[0]看坐标值如果明显偏大就除以wx.getSystemInfoSync().pixelRatio再换算。5. 进阶改造与验证把单机五子棋改成能演示 AI 对战的版本单机双人对战的五子棋作为期末作业够用但如果想让答辩时多讲五分钟技术含量加一个简单的 AI 对手是最划算的改造。不需要上深度学习一个基于评分表的启发式搜索就能让棋力超过大部分随手下的同学。核心思路是对棋盘上每个空位分别计算「如果黑棋下这里能形成什么棋型」和「如果白棋下这里能形成什么棋型」取两者中分值最高的位置落子。棋型评分可以按连子数分档活四两端开放的四连给最高分冲四一端被封的四连次之活三再次活二最低。实现时对每个空位沿四个方向统计同色连子数和两端封堵情况查表得分。下面是一个简化版的评分函数框架// 简化版棋型评分对每个空位打分 const SCORE { FIVE: 100000, OPEN_FOUR: 10000, FOUR: 1000, OPEN_THREE: 1000, THREE: 100, TWO: 10 }; function evaluatePoint(col, row, player) { let total 0; const dirs [[0,1],[1,0],[1,1],[1,-1]]; for (const [dx, dy] of dirs) { let count 1, blocked 0; // 正方向 for (let i 1; i 5; i) { const nc col dx * i, nr row dy * i; if (nc 0 || nc BOARD_SIZE || nr 0 || nr BOARD_SIZE) { blocked; break; } if (board[nr][nc] player) count; else if (board[nr][nc] 0) break; else { blocked; break; } } // 负方向同理此处省略重复逻辑 // 根据 count 和 blocked 查表累加 total } return total; }evaluatePoint返回的是在(col, row)落子对player的价值分。AI 决策时遍历所有空位对黑棋和白棋各算一遍取max(我方得分, 对方得分 * 0.9)的位置——乘 0.9 是让 AI 优先进攻而非纯防守这个系数可以调调高了偏保守调低了偏激进。验证 AI 是否生效最简单的办法是让 AI 执白和人类下几局看它会不会堵活三、会不会自己冲四。如果 AI 第一步下在天元附近说明评分表基本正常如果它到处乱下多半是方向向量或边界检查写错了。改造完之后建议把game.js里的currentPlayer切换逻辑改成人类落子后如果gameOver为 false延迟 300 毫秒调用 AI 落子用setTimeout包一下避免和触摸事件抢执行栈。真机验证时重点看两件事AI 落子后棋盘有没有正确重绘以及连续对局十局以上有没有内存增长——如果board数组是复用的一般不会涨如果每局都新建就得留意了。从那以后我每次拿到一份小游戏源码都强制先跑通「画棋盘 → 落一子 → 判一次胜负」这条最小链路再动其他代码。这份五子棋源码的价值就在于这条链路足够短短到你能在一个晚上把它吃透然后按自己的想法往上加东西。希望帮到你。本文还有配套的精品资源点击获取