原生 JS 实战:300 行代码写五子棋小游戏(含完整源码 + 胜负算法解析)

大家好,我是老周。最近帮实习生做前端入门项目,发现五子棋是最适合练手的小游戏 —— 涵盖 Canvas 绘图、事件监听、二维数组操作等核心知识点,还能玩出优化花样。今天带大家用纯原生技术栈实现,代码可直接复制运行,新手也能 1 小时上手。

一、核心需求与技术选型

先明确开发边界,避免过度设计:

  • 核心功能:棋盘绘制 + 黑白落子 + 胜负判断 + 重新开始
  • 技术栈:HTML5 Canvas(绘图高效)+ TailwindCSS(快速样式)+ 原生 JS(无依赖,便于理解)
  • 避坑提醒:早期试过用 DOM 画格子,15×15 的棋盘就卡得明显,Canvas 的像素操作效率甩 DOM 十条街

二、分步实现(附关键代码)

1. 基础结构搭建(HTML)

用 Canvas 作游戏区,加 2 个控制按钮,结构极简:html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>原生JS五子棋</title>
  6. <!-- 引入Tailwind简化样式 -->
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. </head>
  9. <body >
  10. <div >
  11. <!-- 游戏容器:Canvas必须设宽高属性,不能只靠CSS -->
  12. <canvas id="chessboard" ></canvas>
  13. <div >
  14. <button id="restart" >重新开始</button>
  15. <button id="undo" >悔棋</button>
  16. </div>
  17. <p id="status" >当前回合:黑棋</p>
  18. </div>
  19. <script src="game.js"></script>
  20. </body>
  21. </html>

踩坑点:Canvas 的 / 属性决定绘图分辨率,CSS 样式只负责显示大小,两者不一致会导致图形拉伸。

2. 样式与常量定义(JS 初始化)

先定义全局常量,避免魔法数字:javascript

  1. // 游戏常量(集中管理,便于修改)
  2. const CHESS_SIZE = 30; // 棋子大小
  3. const GRID_COUNT = 15; // 棋盘格子数
  4. const CANVAS_SIZE = CHESS_SIZE * GRID_COUNT; // 画布尺寸
  5. // 获取DOM元素
  6. const canvas = document.getElementById('chessboard');
  7. const ctx = canvas.getContext('2d');
  8. const restartBtn = document.getElementById('restart');
  9. const undoBtn = document.getElementById('undo');
  10. const statusText = document.getElementById('status');
  11. // 游戏状态
  12. let chessBoard = []; // 二维数组存棋盘状态:0空/1黑/2白
  13. let currentPlayer = 1; // 当前玩家:1黑棋/2白棋
  14. let gameOver = false;
  15. let history = []; // 悔棋历史
  16. // 初始化棋盘数组(15×15)
  17. function initChessBoard() {
  18. for (let i = 0; i < GRID_COUNT; i++) {
  19. chessBoard[i] = [];
  20. for (let j = 0; j < GRID_COUNT; j++) {
  21. chessBoard[i][j] = 0;
  22. }
  23. }
  24. history = [];
  25. gameOver = false;
  26. currentPlayer = 1;
  27. statusText.textContent = '当前回合:黑棋';
  28. }

3. 核心绘图逻辑(Canvas 实战)

分棋盘绘制和棋子绘制两部分,用 “中心点定位法” 避免偏移:javascript

  1. // 绘制棋盘网格
  2. function drawBoard() {
  3. ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); // 清空画布
  4. ctx.strokeStyle = '#333';
  5. ctx.lineWidth = 1;
  6. // 画横线和竖线(从边缘留出半个棋子距离)
  7. const offset = CHESS_SIZE / 2;
  8. for (let i = 0; i < GRID_COUNT; i++) {
  9. // 横线
  10. ctx.beginPath();
  11. ctx.moveTo(offset, offset + i * CHESS_SIZE);
  12. ctx.lineTo(CANVAS_SIZE - offset, offset + i * CHESS_SIZE);
  13. ctx.stroke();
  14. // 竖线
  15. ctx.beginPath();
  16. ctx.moveTo(offset + i * CHESS_SIZE, offset);
  17. ctx.lineTo(offset + i * CHESS_SIZE, CANVAS_SIZE - offset);
  18. ctx.stroke();
  19. }
  20. // 画天元和星位(五子棋标准点位)
  21. const starPoints = [
  22. [3, 3], [3, 11], [7, 7], [11, 3], [11, 11]
  23. ];
  24. starPoints.forEach(([x, y]) => {
  25. drawCircle(offset + x * CHESS_SIZE, offset + y * CHESS_SIZE, 3, '#000');
  26. });
  27. }
  28. // 通用画圆函数(复用性设计)
  29. function drawCircle(x, y, radius, color) {
  30. ctx.fillStyle = color;
  31. ctx.beginPath();
  32. ctx.arc(x, y, radius, 0, Math.PI * 2);
  33. ctx.fill();
  34. }
  35. // 绘制棋子
  36. function drawChess(x, y, type) {
  37. const X = CHESS_SIZE / 2 + x * CHESS_SIZE;
  38. const Y = CHESS_SIZE / 2 + y * CHESS_SIZE;
  39. // 棋子渐变效果(比纯色更真实)
  40. const gradient = ctx.createRadialGradient(
  41. X, Y, 0,
  42. X, Y, CHESS_SIZE / 2
  43. );
  44. if (type === 1) { // 黑棋
  45. gradient.addColorStop(0, '#666');
  46. gradient.addColorStop(1, '#000');
  47. } else { // 白棋
  48. gradient.addColorStop(0, '#fff');
  49. gradient.addColorStop(1, '#ccc');
  50. }
  51. drawCircle(X, Y, CHESS_SIZE / 2 - 1, gradient);
  52. }

优化技巧:用createRadialGradient做棋子渐变,比直接填色质感好太多,这是新手容易忽略的细节。

4. 落子与胜负判断(核心算法)

落子要处理 “坐标转换” 和 “重复落子”,胜负判断用 “四点一线检测法”:javascript

  1. // 鼠标点击落子
  2. canvas.addEventListener('click', (e) => {
  3. if (gameOver) return;
  4. // 获取点击的画布坐标(修正偏移)
  5. const rect = canvas.getBoundingClientRect();
  6. const clickX = e.clientX - rect.left;
  7. const clickY = e.clientY - rect.top;
  8. // 转换为棋盘格子坐标(四舍五入取整)
  9. const x = Math.round((clickX - CHESS_SIZE / 2) / CHESS_SIZE);
  10. const y = Math.round((clickY - CHESS_SIZE / 2) / CHESS_SIZE);
  11. // 校验:边界内且空位
  12. if (x >= 0 && x < GRID_COUNT && y >= 0 && y < GRID_COUNT && chessBoard[x][y] === 0) {
  13. chessBoard[x][y] = currentPlayer;
  14. history.push([x, y]); // 记录悔棋历史
  15. drawChess(x, y, currentPlayer);
  16. // 判断胜负
  17. if (checkWin(x, y, currentPlayer)) {
  18. gameOver = true;
  19. statusText.textContent = `${currentPlayer === 1 ? '黑棋' : '白棋'}获胜!点击重新开始`;
  20. return;
  21. }
  22. // 切换玩家
  23. currentPlayer = currentPlayer === 1 ? 2 : 1;
  24. statusText.textContent = `当前回合:${currentPlayer === 1 ? '黑棋' : '白棋'}`;
  25. }
  26. });
  27. // 胜负判断算法(关键!)
  28. function checkWin(x, y, type) {
  29. // 四个方向:水平、垂直、左斜、右斜
  30. const directions = [
  31. [0, 1], // 水平
  32. [1, 0], // 垂直
  33. [1, 1], // 右斜
  34. [1, -1] // 左斜
  35. ];
  36. for (let [dx, dy] of directions) {
  37. let count = 1; // 当前棋子算1个
  38. // 正向遍历
  39. for (let i = 1; i < 5; i++) {
  40. const newX = x + dx * i;
  41. const newY = y + dy * i;
  42. if (newX >= 0 && newX < GRID_COUNT && newY >= 0 && newY < GRID_COUNT && chessBoard[newX][newY] === type) {
  43. count++;
  44. } else {
  45. break;
  46. }
  47. }
  48. // 反向遍历
  49. for (let i = 1; i < 5; i++) {
  50. const newX = x - dx * i;
  51. const newY = y - dy * i;
  52. if (newX >= 0 && newX < GRID_COUNT && newY >= 0 && newY < GRID_COUNT && chessBoard[newX][newY] === type) {
  53. count++;
  54. } else {
  55. break;
  56. }
  57. }
  58. // 五子连珠
  59. if (count >= 5) {
  60. return true;
  61. }
  62. }
  63. return false;
  64. }

算法解析:对落子点的四个方向分别遍历,正向 + 反向计数≥5 即获胜,比暴力遍历整个棋盘效率高,实测 15×15 棋盘响应毫秒级。

5. 附加功能(悔棋 + 重启)

javascript

  1. // 重新开始
  2. restartBtn.addEventListener('click', () => {
  3. initChessBoard();
  4. drawBoard();
  5. });
  6. // 悔棋功能
  7. undoBtn.addEventListener('click', () => {
  8. if (gameOver || history.length === 0) return;
  9. const [x, y] = history.pop();
  10. chessBoard[x][y] = 0; // 清空棋盘状态
  11. drawBoard(); // 重绘棋盘(简单直接,小棋盘够用)
  12. // 恢复上一玩家
  13. currentPlayer = currentPlayer === 1 ? 2 : 1;
  14. statusText.textContent = `当前回合:${currentPlayer === 1 ? '黑棋' : '白棋'}`;
  15. });
  16. // 初始化游戏
  17. initChessBoard();
  18. drawBoard();

注意:这里用重绘棋盘实现悔棋,适合小棋盘;如果做大型游戏,建议用 “脏矩形优化” 局部重绘,减少性能消耗。

三、完整源码与调试技巧

1. 单文件版源码(复制即用)

把 JS 代码嵌入 HTML 的<script>标签,保存为.html直接在浏览器打开:html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>原生JS五子棋</title>
  6. <script src="https://cdn.tailwindcss.com"></script>
  7. </head>
  8. <body >
  9. <div >
  10. <canvas id="chessboard" ></canvas>
  11. <div >
  12. <button id="restart" >重新开始</button>
  13. <button id="undo" >悔棋</button>
  14. </div>
  15. <p id="status" >当前回合:黑棋</p>
  16. </div>
  17. <script>
  18. // 此处粘贴上述所有JS代码
  19. </script>
  20. </body>
  21. </html>

2. 调试避坑指南

  1. 棋子偏移:检查CHESS_SIZE是否整除CANVAS_SIZE,推荐用 30×15=450 的整数组合
  2. 胜负判断失效:在checkWin里加console.log(count),看计数是否正确
  3. 移动端适配:给 Canvas 加touchstart事件,逻辑和click一致,记得阻止默认行为

四、进阶拓展方向(给想深入的同学)

  1. AI 对手:用 “极大极小值算法” 实现简单 AI,核心是评估棋型得分(活四 > 冲四 > 活三)
  2. 联机对战:用 WebSocket 实现双人联机,参考 Istrolid 的网络架构思路
  3. 历史记录:用 localStorage 保存对局记录,支持复盘
  4. 皮肤系统:加载不同棋盘 / 棋子图片,用 Canvas 的drawImage替代绘制

总结

五子棋看似简单,实则涵盖前端游戏开发的核心思维:状态管理(二维数组)+ 图形渲染(Canvas)+ 交互逻辑(事件监听)+ 算法优化(胜负判断)。新手别一开始就贪多,先跑通基础版,再逐步加功能 —— 我当年第一次写的时候,连坐标转换都踩了半天坑,多调试几次自然就懂了。你们做过哪些前端小游戏?或者想给这个五子棋加什么功能?评论区交流,我会抽时间解答!