
简介这是一份面向微信小游戏开发初学者的五子棋单机游戏项目源码覆盖了从项目配置、入口文件到核心游戏逻辑的完整实现适合想要通过短小案例理解小游戏运行机制的读者。整个资源压缩包共包含6个文件以 JavaScript 脚本为主要代码载体配合 JSON 配置文件与 Markdown 说明文档整体仅4KB体量轻、结构清晰方便逐行阅读和二次修改。源码围绕五子棋的核心玩法展开包括棋盘绘制、黑白棋落子、轮换与胜负判定等关键模块通过阅读核心 JS 文件可以熟悉微信小游戏的基本 API 调用、事件响应和代码组织方式同时JSON 格式的项目与游戏配置也有助于初学者理解小游戏项目的默认结构与启动流程。随包附带的说明文档对运行方式和文件作用做了简要交代能降低新手上手时的环境配置门槛。目前已有1960人学习下载对于需要一份精简可运行的起步项目来实践微信小游戏开发的学习者是一份值得参考的入门素材。1. 为什么五子棋是练手微信小游戏的绝佳项目五子棋规则简单、胜负判定清晰却天然包含棋盘建模、实时交互、AI 博弈这三个绝大多数小游戏都要面对的核心问题。拿到一份五子棋微信小游戏源码你不只是读懂一段棋谱逻辑更会理解微信小游戏如何在 Canvas 上绘制界面、如何处理触摸事件、如何在单线程模型里写出不卡顿的循环。对于已经写过几年 Web 或后端业务代码、想转小程序生态的人来说这个项目是性价比最高的第一站。初学者可以照源码画出棋盘、跑通双人对战有经验的开发者则可以重点研究 AI 评分函数、局部渲染优化和微信 SDK 的调用边界。而无论哪类读者最终都会遇到同一个问题商业级小游戏项目源码与 Demo 的差距往往不在玩法而在平台兼容和状态管理。这篇文章会沿着源码结构、核心算法、平台适配、发布避坑四个方向展开使用的技术栈是微信小程序原生环境加一行代码都未依赖第三方库的纯 JavaScript。2. 从零搭起五子棋微信小游戏的源码骨架2.1 微信小游戏环境与 Canvas 渲染选型微信小游戏的运行环境是 JavaScript 引擎但和浏览器有显著差异没有 DOM 和 BOM只能通过wx.createCanvas()获取屏幕画布并且所有渲染必须走 Canvas 2D 或 WebGL。这意味着你在源码里看到的所有document.getElementById都是无效的正确的做法是像下面这样拿到全局画布。// game.js 是微信小游戏的入口文件 import { Game } from ./js/game; const canvas wx.createCanvas(); // 主屏画布 const ctx canvas.getContext(2d); // 获取 2d 上下文 new Game(ctx); // 把画布上下文交给游戏逻辑这段代码的逻辑并不复杂wx.createCanvas()在微信小游戏里只能调用一次它返回的是铺满整个屏幕的顶层画布。后续所有绘制都要通过getContext(2d)得到的ctx对象完成。注意小游戏还有离屏 Canvas 的用法后面讲绘制优化时会提到但入口永远是这一份代码。参数层面的关键点是canvas.width和canvas.height。绝大多数手机的逻辑分辨率是 375 × 667iPhone 6/7/8 的缩放值你可以直接写死也可以用wx.getSystemInfoSync().windowWidth / windowHeight动态获取。我一般推荐后者因为微信小游戏的屏幕适配问题几乎都出在写死分辨率上。源码里如果有adapt()之类的函数多半就是在做这件事。2.2 项目目录与入口文件说明一份规范的微信小游戏源码顶层结构通常只有固定的几个文件。你不需要创建 HTML也不存在 CSS。整个项目的启动依赖game.json、game.js和若干个 JS 模块。下面是一个最常见的最小目录树├── game.js # 入口创建画布并启动游戏循环 ├── game.json # 小游戏全局配置横屏/竖屏、设备方向 ├── project.config.json # 微信开发者工具的工程配置 ├── js/ │ ├── core/ │ │ ├── board.js # 棋盘数据模型 │ │ ├── judge.js # 胜负判定 │ │ └── ai.js # 电脑对手的评分逻辑 │ └── ui/ │ ├── renderer.js # 绘制棋盘与棋子 │ └── touch.js # 触摸事件解析 └── images/ # 静态资源如果有背景图或棋子贴图game.json里面最容易被新作者忽略的是deviceOrientation字段它决定游戏是竖屏还是横屏。五子棋一般竖屏即可但如果你发现源码里画布旋转了 90 度多半是这里写成了landscape。{ deviceOrientation: portrait, showStatusBar: false, networkTimeout: { request: 5000 } }这三个字段值得解释deviceOrientation控制屏幕方向showStatusBar是否显示系统状态栏networkTimeout配置网络请求超时。对于纯本地对战的五子棋项目你可以完全忽略networkTimeout但如果源码里包含联机功能就必须检查这一项否则某些 Android 机上请求超时时间过长用户会以为游戏卡死了。2.3 游戏主循环与帧率控制微信小游戏没有浏览器那样的requestAnimationFrame标准实现但小游戏环境提供了一个requestAnimationFrame接口。它的行为和浏览器的基本一致但需要注意分辨率渲染的时间点。一个典型的游戏循环如下class Game { constructor(ctx) { this.ctx ctx; this.board new Board(15); this.frameHandle null; this.lastTime 0; } start() { this.frameHandle requestAnimationFrame(this.loop.bind(this)); } loop(timestamp) { this.update(timestamp - this.lastTime); this.render(this.ctx); this.lastTime timestamp; this.frameHandle requestAnimationFrame(this.loop.bind(this)); } update(deltaTime) { // 处理逻辑deltaTime 是距离上一帧的毫秒数 } render(ctx) { // 绘制棋盘和棋子 } }deltaTime参数特别重要。如果你直接拿timestamp去做棋子移动计算那么高刷新率手机比如 120Hz上的动画速度会比 60Hz 手机快一倍。用timestamp - lastTime得到的毫秒差才能保证所有设备上的表现一致。所以当你拿到源码时先看它的loop函数里有没有传增量时间没有的话要自己补上。3. 五子棋核心逻辑与 AI 算法实现3.1 棋盘数据模型与落子规则五子棋棋盘一般是 15 × 15数据模型可以是一个二维数组。数组索引从 0 开始值为 0 表示空位1 表示黑子2 表示白子。这种模型简单而且后面做 AI 评分时可以直接遍历数组。class Board { constructor(size 15) { this.size size; this.grid new Array(size).fill(0).map(() new Array(size).fill(0)); this.currentPlayer 1; // 1 为黑2 为白 } reset() { for (let i 0; i this.size; i) { for (let j 0; j this.size; j) { this.grid[i][j] 0; } } this.currentPlayer 1; } place(row, col) { if (this.grid[row][col] ! 0) { return false; } this.grid[row][col] this.currentPlayer; return true; } }place方法里的边界检查是必须的当用户点击棋盘边缘时row和col可能越界也可能落在已有棋子上。很多新手源码只判断了grid[row][col] 0忘了判断行列是否在 0 到 14 之间结果就是下标越界导致整个页面白屏。在微信小游戏环境里这类异常不会像浏览器那样弹出提示而是直接静默失败排查时只能靠wx.onError回调打印日志。双人对战的规则不需要额外维护落子方因为currentPlayer会在每次place成功之后翻转。但如果你要加入悔棋功能最好把每一步的坐标存进一个栈里悔棋时弹出栈顶并恢复currentPlayer。不过这里要注意AI 对战模式下一旦悔棋你需要同时撤销 AI 的一步否则棋盘状态会错乱。3.2 胜负判定四方向扫描与方向向量胜负判定最容易想到的做法是遍历整个棋盘对每个坐标检查横、竖、左斜、右斜四个方向是否存在连续五子。更高效的方式是只判断最后落子的位置因为新棋子的落点才是改变棋盘状态的唯一位置。function checkWin(board, row, col) { const player board.grid[row][col]; const directions [ { dx: 0, dy: 1 }, // 竖 { dx: 1, dy: 0 }, // 横 { dx: 1, dy: 1 }, // 右斜 { dx: 1, dy: -1 } // 左斜 ]; for (let dir of directions) { let count 1; // 正方向数 for (let i 1; i 5; i) { const r row dir.dx * i; const c col dir.dy * i; if (r 0 || r board.size || c 0 || c board.size) break; if (board.grid[r][c] ! player) break; count; } // 反方向数 for (let i 1; i 5; i) { const r row - dir.dx * i; const c col - dir.dy * i; if (r 0 || r board.size || c 0 || c board.size) break; if (board.grid[r][c] ! player) break; count; } if (count 5) { return player; } } return null; }这段代码的关键在于directions数组的表达。每个方向用一个单位向量(dx, dy)描述然后正方向乘1,2,3,4反方向乘-1,-2,-3,-4。这里有个很容易写错的点判断边界时必须同时判断行和列不能只判断其中一个否则数组访问会越界。另一个容易被忽略的是count 5虽然标准五子棋超过五个算赢但也有的规则要求长连不算赢那么你需要在count 5时返回具体看项目需求。3.3 简单高效的 AI 评分函数五子棋 AI 算法有很多种极大极小搜索、蒙特卡洛树搜索、遗传算法。但对于普通小游戏项目来说一个基于权重表的评分函数已经足够让玩家觉得“挺聪明”。核心思想是遍历每个空位分别计算该位置对己方和对方的得分然后加权求和。防守和进攻的权重通常设为接近的值否则 AI 表现会过于激进或过于懦弱。class AI { constructor(board) { this.board board; // 连子数量对应的基础分数 this.scoreTable { 1: 10, // 一个子 2: 100, // 两个连子 3: 1000, // 三个连子 4: 10000, // 四个连子 5: 100000 // 必定胜利 }; } evaluate(player) { let totalScore 0; // 对四个方向进行扫描 const directions [ { dx: 0, dy: 1 }, { dx: 1, dy: 0 }, { dx: 1, dy: 1 }, { dx: 1, dy: -1 } ]; for (let row 0; row this.board.size; row) { for (let col 0; col this.board.size; col) { if (this.board.grid[row][col] ! 0) continue; for (let dir of directions) { let count 1; let block 0; // 0 表示通畅1 表示一边被堵2 表示两边都被堵或边界 // 正方向 for (let i 1; i 6; i) { const r row dir.dx * i; const c col dir.dy * i; if (r 0 || r this.board.size || c 0 || c this.board.size) { block; break; } if (this.board.grid[r][c] player) { count; } else if (this.board.grid[r][c] ! 0) { block; break; } } // 反方向 for (let i 1; i 6; i) { const r row - dir.dx * i; const c col - dir.dy * i; if (r 0 || r this.board.size || c 0 || c this.board.size) { block; break; } if (this.board.grid[r][c] player) { count; } else if (this.board.grid[r][c] ! 0) { block; break; } } if (count 5) totalScore this.scoreTable[5]; else if (block 0) totalScore this.scoreTable[count]; else if (block 1) totalScore this.scoreTable[count] * 0.5; // block 2 时连子完全被封死不加分 } } } return totalScore; } getBestMove() { let bestScore -Infinity; let bestMove null; // 遍历所有空位计算我方和对方分别的分数 for (let row 0; row this.board.size; row) { for (let col 0; col this.board.size; col) { if (this.board.grid[row][col] ! 0) continue; this.board.grid[row][col] this.board.currentPlayer; // 假设自己下在这里 let myScore this.evaluate(this.board.currentPlayer); this.board.grid[row][col] this.board.currentPlayer 1 ? 2 : 1; // 假设对方下在这里 let oppScore this.evaluate(this.board.currentPlayer 1 ? 2 : 1); this.board.grid[row][col] 0; let score myScore * 0.6 oppScore * 0.4; // 进攻权重略高 if (score bestScore) { bestScore score; bestMove { row, col }; } } } return bestMove; } }这个 AI 实现的时间复杂度是 O(n^2 × 4 × 5)对于 15 × 15 的棋盘来说大约 9000 次计算在主流手机上能在几毫秒内跑完完全不用放到 Worker 线程。真正的性能问题在于getBestMove里面反复调用evaluate如果 AI 难度需要更高可以提前缓存每个空位的分数但那样代码会复杂很多。对于源码学习者而言这个版本足够看懂后再去扩展。攻防权重参数0.6和0.4是经验值实际效果是 AI 会优先保证自己成五然后在防守上稍有滞后。你可以把myScore的权重调成0.8让 AI 更激进或者把oppScore权重调成0.6让它更龟缩防守。同一个源码改这两个数就能做出不同难度这也是面试官常问的“你的 AI 怎么调难度”的答案之一。4. 微信小游戏平台适配与交互细节4.1 触摸事件与坐标换算微信小游戏没有 DOM 事件触摸事件要通过wx.onTouchStart全局注册。但这里有个陷阱你拿到的触摸坐标是相对屏幕的物理坐标而棋盘绘制时可能使用了ctx.scale()或者ctx.translate()所以必须把屏幕坐标换算成棋盘坐标。// 在 Game 构造函数中注册 wx.onTouchStart((event) { const touch event.touches[0]; const { x, y } touch; // 如果你之前设置了画布左上角偏移 boardOffsetX/Y以及格子大小 cellSize const row Math.floor((y - boardOffsetY) / cellSize); const col Math.floor((x - boardOffsetX) / cellSize); if (row 0 row 15 col 0 col 15) { if (this.board.place(row, col)) { this.onMove(row, col); } } });boardOffsetX和boardOffsetY是棋盘左上角在画布上的像素坐标。有些源码会把棋盘居中绘制那么这两个值需要在屏幕宽高变化时重新计算。一个常见的做法是写一个recalculateLayout函数在wx.onWindowResize回调里更新布局参数。虽然大部分手机窗口大小不会变但 iPad 上旋转屏幕时窗口尺寸一定会变。另外一点event.touches[0]并不总是标准的坐标轴。有些 Android 设备在触摸事件里还包含identifier和force字段但x和y依然是文档里明确给出的。不要依赖clientX或pageX那些是浏览器属性小游戏里没有。4.2 绘制优化离屏 Canvas 与脏矩形五子棋的棋盘是静态的只有落子后才需要重绘。如果每次render都清空整个画布再画 15 × 15 条线在低端 Android 机会有可见的闪烁。最好的优化方式是使用离屏 Canvas 预绘制棋盘背景。const offscreen wx.createCanvas(); // 注意第二次调用 createCanvas 得到的是离屏画布 offscreen.width canvas.width; offscreen.height canvas.height; const offCtx offscreen.getContext(2d); // 绘制棋盘线、星位、边界等 function drawStaticBoard(ctx) { ctx.lineWidth 2; ctx.strokeStyle #333; for (let i 0; i 15; i) { const x boardOffsetX i * cellSize; const y boardOffsetY i * cellSize; ctx.beginPath(); ctx.moveTo(boardOffsetX, y); ctx.lineTo(boardOffsetX 14 * cellSize, y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x, boardOffsetY); ctx.lineTo(x, boardOffsetY 14 * cellSize); ctx.stroke(); } } drawStaticBoard(offCtx); // 主渲染循环把离屏画布绘制到主画布上 function render(ctx) { ctx.drawImage(offscreen, 0, 0); // 再绘制所有棋子棋子可以只维护一个数组遍历并绘制 this.pieces.forEach((piece) { ctx.beginPath(); ctx.arc(piece.x, piece.y, cellSize * 0.45, 0, Math.PI * 2); ctx.fillStyle piece.player 1 ? #000 : #fff; ctx.fill(); ctx.strokeStyle piece.player 1 ? #000 : #ccc; ctx.stroke(); }); }这里的关键是wx.createCanvas()第一次调用返回主屏画布之后的调用返回离屏画布。很多源码都会有一句注释const screenCanvas wx.createCanvas()然后在onShow里再次调用wx.createCanvas()这就错了——第二次调用拿到的不是主画布而是一块独立的内存画布所有绘制都不会显示到屏幕上。进一步优化是只重绘发生变化的小区域但五子棋的棋盘本身已经有离屏缓存棋子重绘也不多所以脏矩形带来的收益有限。如果你发现落子后画面有残影真正原因往往是忘了把该位置旧棋子清除而不是因为矩形范围不够“脏”。4.3 音效、分享与排行榜接入微信小游戏给开发者提供了很多小程序的特色能力。在五子棋源码里至少会用到三个音效播放、右上角分享、关系链数据存储。// 音效 const ctx wx.createInnerAudioContext(); ctx.src audio/chess.mp3; ctx.play(); // 分享 wx.onShareAppMessage(() { return { title: 五子棋挑战你能赢过我吗, imageUrl: share.png }; }); // 保存最高分 wx.setStorageSync(maxScore, 42);wx.createInnerAudioContext()返回的音频对象需要手动销毁不然会占用内存。分享接口onShareAppMessage必须在用户点击右上角菜单时才会触发回调如果你在源码里看到自动分享按钮那一定是用了wx.showShareMenu加上用户主动点击。这里提醒一点微信小游戏的音效文件格式只支持 mp3、aac、wav而且首次加载需要时间最好在用户第一次触摸屏幕时就预加载音频避免落子瞬间才去读文件造成短暂白屏。排行榜或者存档功能如果只是本地单机版用wx.setStorageSync就足够了。如果要搞实时对战就得用wx.setUserCloudStorage或者云开发数据库但那已经超出了“五子棋源码”的范围可以当作后续扩展方向。5. 打包发布时的常见踩坑与一个必会优化5.1 微信开发者工具打包与上传在微信开发者工具里点击“上传”按钮填上版本号和备注就能把源码包传到微信后台。但上传前必须确认两个配置project.config.json里的appid必须是你自己的小游戏 AppIDgame.json里的deviceOrientation要和实际 UI 匹配。如果 AppID 不对上传会直接报错如果屏幕方向错误审核人员打开游戏时看到黑边可能直接拒绝。另一个坑是代码体积。微信小游戏包体限制是 4MB超出后不能上传。五子棋源码通常只有几十 KB一般不会超但如果加入了较大的图片素材或者音频文件就要注意压缩图片为 WebP 或 JPG 格式音频转为码率更低的 mp3。常见做法是把所有图片放到images/目录使用wx.getFileSystemManager()读取本地文件而不是把图片转成 base64 写死到 JS 里那样会让代码包膨胀好几倍。5.2 著作权登记与审核避坑从最近的规则来看微信小游戏上线前需要提供《计算机软件著作权登记证书》或者源代码备查。具体的审核条款变化很快我们作为开发者最好在发布前先到微信公众平台查看最新要求。一线实操中很多人第一次提审是因为没填“游戏类目”而被拒绝。解决方案是在game.json里增加category: game字段并且注意游戏名称不要包含“官方”“唯一”等违规词语。如果你的五子棋源码中使用了网络请求比如排行榜数据来自自己的服务器那么必须在申请里开启一些权限并配置合法域名。审核时如果发现域名没有备案会直接驳回。所以对于学习项目我建议直接在本地存储最高分等真正想要做日活留存再上云服务。5.3 一个技巧用单例管理全局状态避免内存泄漏最后分享一个我在阅读五子棋源码时特别看重的小细节全局状态对象的管理。很多人直接在game.js里定义var board new Board()然后在game.js里引用但模块化的 JS 文件之间互相引用时容易造成循环依赖。// state.js class State { constructor() { this.board null; this.currentPlayer 1; this.history []; } } State.getInstance function() { if (!State.instance) { State.instance new State(); } return State.instance; }; module.exports State;在任何模块里调用State.getInstance()拿到的都是同一个实例这样judge.js、ai.js、renderer.js之间不需要互相传递board引用。这个模式在微信小游戏里特别实用因为小游戏环境的global对象并不像 Node.js 那样可靠而模块级静态变量在分包加载或热重启时容易被重置。用单例加上wx.onHide里保存当前局面玩家切后台回来时可以直接恢复到之前的对局体验会好很多。另外单例的另一个好处是方便做撤销和复盘。如果你把每一步坐标都 push 到history数组中再在 UI 上放一个“悔棋”按钮多端共享数据时就不会因为传参顺序不同而出错。这个小改动可以让你的源码从“能玩”变成“像一个正规产品”。试试看这也许就是你的开源作品被其他人 star 的原因。本文还有配套的精品资源点击获取