围棋对战游戏开发详解 附源码
基于HTML5 Canvas的围棋对战游戏开发详解
相关资源已经放在 github 上,欢迎star or fork
https://github.com/chenchihwen/weiqi-gamehttps://github.com/chenchihwen/weiqi-game
在线使用
围棋对战https://chenchihwen.github.io/weiqi-game/
项目概述
这是一个使用原生JavaScript和HTML5 Canvas开发的两人围棋对战游戏,完整实现了标准19×19围棋棋盘的核心功能。项目采用面向对象设计模式,代码结构清晰,易于维护和扩展。
核心功能特性
-
✅ 标准围棋规则:完整实现19×19棋盘、黑白轮流下棋
-
✅ 智能吃子系统:基于连通区域算法的精确吃子判定
-
✅ 自杀规则检测:防止无效落子,确保游戏规则正确性
-
✅ 悔棋功能:支持撤销操作,提升游戏体验
-
✅ 实时统计:动态显示双方棋子数量和被吃统计
-
✅ 美观界面:仿真木质棋盘,带阴影效果的立体棋子
项目结构
wq/ ├── index.html # 主页面文件,包含游戏界面布局 ├── weiqi.js # 游戏核心逻辑实现 └── README.md # 项目说明文档
核心代码解析
1. 游戏初始化 (WeiQiGame
类)
class WeiQiGame {constructor() {this.canvas = document.getElementById('gameCanvas');this.ctx = this.canvas.getContext('2d');this.boardSize = 19; // 19x19标准棋盘this.cellSize = 30; // 每个格子的像素大小this.margin = 30; // 棋盘边距this.currentPlayer = 'black'; // 当前玩家this.board = Array(19).fill().map(() => Array(19).fill(null)); // 棋盘状态this.moveHistory = []; // 走棋历史记录// 统计信息this.stats = {blackCaptured: 0, // 黑棋被吃数量whiteCaptured: 0, // 白棋被吃数量blackOnBoard: 0, // 黑棋在盘数量whiteOnBoard: 0 // 白棋在盘数量};} }
2. 棋盘绘制
使用Canvas API绘制棋盘网格和星位点:
drawBoard() {// 绘制棋盘背景this.ctx.fillStyle = '#DEB887'; // 木质颜色this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);// 绘制网格线for (let i = 0; i < this.boardSize; i++) {// 垂直线this.ctx.beginPath();this.ctx.moveTo(this.margin + i * this.cellSize, this.margin);this.ctx.lineTo(this.margin + i * this.cellSize, this.margin + (this.boardSize - 1) * this.cellSize);this.ctx.stroke(); // 水平线this.ctx.beginPath();this.ctx.moveTo(this.margin, this.margin + i * this.cellSize);this.ctx.lineTo(this.margin + (this.boardSize - 1) * this.cellSize, this.margin + i * this.cellSize);this.ctx.stroke();}// 绘制星位点const starPoints = [[3, 3], [3, 9], [3, 15],[9, 3], [9, 9], [9, 15],[15, 3], [15, 9], [15, 15]];starPoints.forEach(([x, y]) => {this.ctx.beginPath();this.ctx.arc(this.margin + x * this.cellSize,this.margin + y * this.cellSize,3, 0, 2 * Math.PI);this.ctx.fill();}); }
3. 棋子绘制
实现带阴影效果的棋子渲染:
drawStone(x, y, color) {const centerX = this.margin + x * this.cellSize;const centerY = this.margin + y * this.cellSize;const radius = this.cellSize * 0.4; // 绘制棋子阴影this.ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';this.ctx.beginPath();this.ctx.arc(centerX + 2, centerY + 2, radius, 0, 2 * Math.PI);this.ctx.fill(); // 绘制棋子if (color === 'black') {this.ctx.fillStyle = '#000';} else {this.ctx.fillStyle = '#fff';this.ctx.strokeStyle = '#000';this.ctx.lineWidth = 1;} this.ctx.beginPath();this.ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);this.ctx.fill(); if (color === 'white') {this.ctx.stroke();} }
4. 游戏规则实现
吃子逻辑
makeMove(col, row) {// 检查该位置是否已有棋子if (this.board[row][col] !== null) {this.updateStatus('该位置已有棋子!');return false;} // 临时下棋,用于检查是否合法this.board[row][col] = this.currentPlayer; // 检查并移除被吃掉的对方棋子const opponent = this.currentPlayer === 'black' ? 'white' : 'black';let capturedStones = 0; // 检查四个方向的对方棋子群是否被吃const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newRow = row + dy;const newCol = col + dx; if (this.isValidPosition(newCol, newRow) &&this.board[newRow][newCol] === opponent) { const group = this.getGroup(newCol, newRow);if (this.getLiberties(group).length === 0) {// 这个对方棋子群没有气,被吃掉for (let stone of group) {this.board[stone.row][stone.col] = null;capturedStones++;} // 更新被吃统计if (opponent === 'black') {this.stats.blackCaptured += group.length;} else {this.stats.whiteCaptured += group.length;}}}} // 检查自己的棋子是否自杀(下完后自己没有气)const myGroup = this.getGroup(col, row);const myLiberties = this.getLiberties(myGroup); if (myLiberties.length === 0 && capturedStones === 0) {// 自杀手,不允许this.board[row][col] = null;this.updateStatus('不能自杀!');return false;} }
棋子群与气的计算
// 获取连通的棋子群 getGroup(col, row) {const color = this.board[row][col];if (!color) return []; const group = [];const visited = new Set();const stack = [{ col, row }]; while (stack.length > 0) {const { col: currentCol, row: currentRow } = stack.pop();const key = `${currentCol},${currentRow}`; if (visited.has(key)) continue;visited.add(key); if (this.board[currentRow][currentCol] === color) {group.push({ col: currentCol, row: currentRow }); // 检查四个方向const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newCol = currentCol + dx;const newRow = currentRow + dy;const newKey = `${newCol},${newRow}`; if (this.isValidPosition(newCol, newRow) && !visited.has(newKey)) {stack.push({ col: newCol, row: newRow });}}}} return group; } // 获取棋子群的气(空位) getLiberties(group) {const liberties = new Set(); for (let stone of group) {const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newCol = stone.col + dx;const newRow = stone.row + dy; if (this.isValidPosition(newCol, newRow) &&this.board[newRow][newCol] === null) {liberties.add(`${newCol},${newRow}`);}}} return Array.from(liberties).map(key => {const [col, row] = key.split(',').map(Number);return { col, row };}); }
5. 游戏功能
悔棋功能
undoMove() {if (this.moveHistory.length === 0) {this.updateStatus('没有可以悔棋的步数');return;} // 移除最后一步this.moveHistory.pop(); if (this.moveHistory.length > 0) {// 恢复到上一步的棋盘状态const lastMove = this.moveHistory[this.moveHistory.length - 1];this.board = lastMove.board.map(row => [...row]);this.currentPlayer = lastMove.player === 'black' ? 'white' : 'black';} else {// 如果没有历史记录,重置棋盘this.board = Array(this.boardSize).fill().map(() => Array(this.boardSize).fill(null));this.currentPlayer = 'black';} // 重新计算统计信息this.recalculateStats(); }
重新开始游戏
resetGame() {this.board = Array(this.boardSize).fill().map(() => Array(this.boardSize).fill(null));this.currentPlayer = 'black';this.moveHistory = []; // 重置统计this.stats = {blackCaptured: 0,whiteCaptured: 0,blackOnBoard: 0,whiteOnBoard: 0}; this.drawBoard();this.updatePlayerDisplay();this.updateStats();this.updateStatus('游戏重新开始'); }
界面设计
-
采用传统的木质棋盘风格(#DEB887背景色)
-
棋子带有阴影效果增强立体感
-
实时显示当前玩家和游戏状态
-
统计信息面板显示黑白双方在盘和被吃棋子数
-
简洁的控制按钮(悔棋、重新开始)
技术亮点
-
纯前端实现:仅使用HTML/CSS/JavaScript,无需后端支持
-
面向对象设计:使用ES6类组织代码,结构清晰
-
Canvas优化:高效的重绘策略,只在必要时更新画布
-
完整规则实现:包括吃子、自杀规则等围棋核心规则
-
状态管理:通过深拷贝保存棋盘历史状态,实现悔棋功能
-
事件处理:精确的鼠标点击位置计算,实现棋盘交叉点定位
核心算法分析
1. 连通区域搜索算法
算法类型:深度优先搜索(DFS) 时间复杂度:O(n),其中n为棋子群大小 空间复杂度:O(n),用于存储访问状态和递归栈
核心思想:从一个棋子开始,递归搜索所有相邻的同色棋子,形成连通区域。
2. 气的计算算法
数据结构:Set集合(自动去重) 时间复杂度:O(m×4),其中m为棋子群大小 优化策略:使用Set避免重复计算相同的空位
3. 吃子判定算法
判定条件:棋子群的气数量为0 执行时机:每次落子后检查对方棋子群 处理流程:检测→移除→更新统计
4. 自杀规则检测
检测逻辑:落子后自身棋子群无气且未吃掉对方棋子 防护机制:预先模拟落子,检测合法性后再执行
功能扩展规划
🤖 AI对战模块
-
算法选择:蒙特卡洛树搜索(MCTS) + 神经网络评估
-
难度分级:初级、中级、高级三个AI等级
-
实现方案:Web Workers处理AI计算,避免界面卡顿
-
预计工作量:2-3周开发周期
⏱️ 计时系统
-
基础计时:总时间限制 + 单步时间限制
-
读秒功能:最后30秒倒计时提醒
-
时间规则:支持多种时间制(快棋、慢棋、超快棋)
-
技术实现:setInterval + Web Audio API音效提醒
📋 棋谱管理
-
格式支持:SGF(Smart Game Format)标准格式
-
功能特性:保存、加载、回放、分享棋谱
-
存储方案:LocalStorage本地存储 + 云端同步选项
-
导出功能:支持导出为图片、PDF格式
🌐 网络对战
-
通信协议:WebSocket实时双向通信
-
房间系统:创建房间、加入房间、观战模式
-
匹配机制:随机匹配 + 好友对战
-
后端技术:Node.js + Socket.io
🎯 智能判定
-
死活判定:基于深度学习的死活识别算法
-
目数计算:自动计算双方领地和目数
-
终局检测:双方连续pass后自动进入计算阶段
-
争议处理:手动调整 + 申诉机制
使用方法
-
下载项目代码
-
直接在浏览器中打开
index.html
-
点击棋盘交叉点下棋
-
使用底部按钮控制游戏(悔棋、重新开始)
性能优化
-
选择性重绘:只在必要时重绘棋盘,减少不必要的Canvas操作
-
事件委托:使用单一事件监听器处理所有棋盘点击
-
数据结构优化:使用Set进行气的去重计算,提高效率
-
算法优化:在吃子判断中避免重复计算连通区域
项目总结与技术价值
🎯 技术成果
这个围棋项目成功展示了如何使用纯前端技术栈开发复杂的策略类游戏。项目在以下几个方面具有重要的技术价值:
-
算法实现价值
-
深度优先搜索在游戏开发中的实际应用
-
连通区域检测算法的高效实现
-
复杂游戏规则的程序化表达
-
-
工程实践价值
-
面向对象设计在JavaScript中的最佳实践
-
Canvas图形编程的性能优化技巧
-
事件驱动架构的合理运用
-
-
用户体验价值
-
流畅的交互体验设计
-
直观的视觉反馈机制
-
完整的游戏功能闭环
-
🔧 核心技术难点突破
难点一:围棋规则的程序化实现
-
通过数学建模将抽象的围棋规则转化为具体的算法逻辑
-
使用图论算法解决连通性问题
-
实现了业界标准的围棋规则引擎
难点二:高性能Canvas渲染
-
采用选择性重绘策略,避免不必要的性能消耗
-
实现了平滑的动画效果和视觉反馈
-
优化了大量棋子的渲染性能
难点三:复杂状态管理
-
设计了完整的游戏状态机
-
实现了可靠的历史记录和回滚机制
-
保证了数据一致性和操作原子性
🚀 项目亮点与创新
-
零依赖纯净实现:不依赖任何第三方库,展示了原生Web技术的强大能力
-
算法驱动的游戏逻辑:将计算机科学理论完美应用于实际项目
-
可扩展的架构设计:为后续功能扩展预留了充足的架构空间
-
工业级代码质量:代码结构清晰,注释完整,易于维护
📈 学习价值与适用场景
适合学习的开发者:
-
前端开发初学者:学习Canvas API和JavaScript面向对象编程
-
算法爱好者:了解图论算法在实际项目中的应用
-
游戏开发者:掌握Web游戏开发的基本技巧和最佳实践
可复用的技术方案:
-
其他棋类游戏开发(象棋、五子棋等)
-
网格类益智游戏开发
-
Canvas图形应用开发
-
算法可视化项目
🎓 技术启发与思考
这个项目证明了原生Web技术在复杂应用开发中的可行性。在当今框架盛行的时代,回归原生技术不仅能够加深对底层原理的理解,还能够开发出性能优异、体积轻量的应用。
对于想要深入学习前端技术的开发者来说,这个项目提供了一个理论与实践相结合的绝佳案例。通过分析和改进这个项目,可以全面提升算法思维、工程能力和产品意识。
希望这个项目能够启发更多开发者探索Web技术的无限可能! 🌟
源码
前端
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>围棋对战</title><style>body {margin: 0;padding: 20px;font-family: Arial, sans-serif;background-color: #f5f5f5;display: flex;flex-direction: column;align-items: center;}.game-container {background-color: white;border-radius: 10px;padding: 20px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);}.game-info {text-align: center;margin-bottom: 20px;}.current-player {font-size: 24px;font-weight: bold;margin-bottom: 10px;}.black-turn {color: #333;}.white-turn {color: #666;}canvas {border: 2px solid #8B4513;background-color: #DEB887;cursor: crosshair;}.controls {text-align: center;margin-top: 20px;}button {padding: 10px 20px;font-size: 16px;margin: 0 10px;border: none;border-radius: 5px;cursor: pointer;background-color: #4CAF50;color: white;}button:hover {background-color: #45a049;}.reset-btn {background-color: #f44336;}.reset-btn:hover {background-color: #da190b;}.game-status {margin-top: 15px;font-size: 14px;color: #666;}.stats-container {margin-top: 20px;padding: 15px;background-color: #f9f9f9;border-radius: 8px;border: 1px solid #ddd;}.stats-row {display: flex;justify-content: space-between;margin-bottom: 8px;}.stats-row:last-child {margin-bottom: 0;}.stat-item {display: flex;align-items: center;min-width: 120px;}.stat-label {font-size: 14px;color: #555;margin-right: 8px;}.stat-value {font-size: 16px;font-weight: bold;color: #333;min-width: 20px;text-align: center;background-color: white;padding: 2px 8px;border-radius: 4px;border: 1px solid #ccc;}</style>
</head>
<body><div class="game-container"><div class="game-info"><h1>围棋对战</h1><div class="current-player" id="currentPlayer">当前玩家: 黑棋</div><div class="game-status" id="gameStatus">点击棋盘下棋</div><!-- 统计信息 --><div class="stats-container"><div class="stats-row"><div class="stat-item"><span class="stat-label">黑棋在盘:</span><span class="stat-value" id="blackOnBoard">0</span></div><div class="stat-item"><span class="stat-label">白棋在盘:</span><span class="stat-value" id="whiteOnBoard">0</span></div></div><div class="stats-row"><div class="stat-item"><span class="stat-label">黑棋被吃:</span><span class="stat-value" id="blackCaptured">0</span></div><div class="stat-item"><span class="stat-label">白棋被吃:</span><span class="stat-value" id="whiteCaptured">0</span></div></div></div></div><canvas id="gameCanvas" width="600" height="600"></canvas><div class="controls"><button onclick="resetGame()" class="reset-btn">重新开始</button><button onclick="undoMove()">悔棋</button></div></div><script src="weiqi.js"></script>
</body>
</html>
后端
class WeiQiGame {constructor() {this.canvas = document.getElementById('gameCanvas');this.ctx = this.canvas.getContext('2d');this.boardSize = 19;this.cellSize = 30;this.margin = 30;this.currentPlayer = 'black'; // 'black' or 'white'this.board = Array(this.boardSize).fill().map(() => Array(this.boardSize).fill(null));this.moveHistory = [];// 统计信息this.stats = {blackCaptured: 0, // 黑棋被吃数量whiteCaptured: 0, // 白棋被吃数量blackOnBoard: 0, // 黑棋在盘数量whiteOnBoard: 0 // 白棋在盘数量};this.init();}init() {this.drawBoard();this.canvas.addEventListener('click', (e) => this.handleClick(e));this.updatePlayerDisplay();this.updateStats();}drawBoard() {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);// 绘制棋盘背景this.ctx.fillStyle = '#DEB887';this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);// 绘制网格线this.ctx.strokeStyle = '#000';this.ctx.lineWidth = 1;for (let i = 0; i < this.boardSize; i++) {// 垂直线this.ctx.beginPath();this.ctx.moveTo(this.margin + i * this.cellSize, this.margin);this.ctx.lineTo(this.margin + i * this.cellSize, this.margin + (this.boardSize - 1) * this.cellSize);this.ctx.stroke();// 水平线this.ctx.beginPath();this.ctx.moveTo(this.margin, this.margin + i * this.cellSize);this.ctx.lineTo(this.margin + (this.boardSize - 1) * this.cellSize, this.margin + i * this.cellSize);this.ctx.stroke();}// 绘制星位点const starPoints = [[3, 3], [3, 9], [3, 15],[9, 3], [9, 9], [9, 15],[15, 3], [15, 9], [15, 15]];this.ctx.fillStyle = '#000';starPoints.forEach(([x, y]) => {this.ctx.beginPath();this.ctx.arc(this.margin + x * this.cellSize,this.margin + y * this.cellSize,3, 0, 2 * Math.PI);this.ctx.fill();});// 绘制已下的棋子this.drawStones();}drawStones() {for (let row = 0; row < this.boardSize; row++) {for (let col = 0; col < this.boardSize; col++) {if (this.board[row][col]) {this.drawStone(col, row, this.board[row][col]);}}}}drawStone(x, y, color) {const centerX = this.margin + x * this.cellSize;const centerY = this.margin + y * this.cellSize;const radius = this.cellSize * 0.4;// 绘制棋子阴影this.ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';this.ctx.beginPath();this.ctx.arc(centerX + 2, centerY + 2, radius, 0, 2 * Math.PI);this.ctx.fill();// 绘制棋子if (color === 'black') {this.ctx.fillStyle = '#000';} else {this.ctx.fillStyle = '#fff';this.ctx.strokeStyle = '#000';this.ctx.lineWidth = 1;}this.ctx.beginPath();this.ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);this.ctx.fill();if (color === 'white') {this.ctx.stroke();}}handleClick(e) {const rect = this.canvas.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;// 计算最近的交叉点const col = Math.round((x - this.margin) / this.cellSize);const row = Math.round((y - this.margin) / this.cellSize);// 检查是否在棋盘范围内if (col >= 0 && col < this.boardSize && row >= 0 && row < this.boardSize) {this.makeMove(col, row);}}makeMove(col, row) {// 检查该位置是否已有棋子if (this.board[row][col] !== null) {this.updateStatus('该位置已有棋子!');return false;}// 临时下棋,用于检查是否合法this.board[row][col] = this.currentPlayer;// 检查并移除被吃掉的对方棋子const opponent = this.currentPlayer === 'black' ? 'white' : 'black';let capturedStones = 0;// 检查四个方向的对方棋子群是否被吃const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newRow = row + dy;const newCol = col + dx;if (this.isValidPosition(newCol, newRow) &&this.board[newRow][newCol] === opponent) {const group = this.getGroup(newCol, newRow);if (this.getLiberties(group).length === 0) {// 这个对方棋子群没有气,被吃掉for (let stone of group) {this.board[stone.row][stone.col] = null;capturedStones++;}// 更新被吃统计if (opponent === 'black') {this.stats.blackCaptured += group.length;} else {this.stats.whiteCaptured += group.length;}}}}// 检查自己的棋子是否自杀(下完后自己没有气)const myGroup = this.getGroup(col, row);const myLiberties = this.getLiberties(myGroup);if (myLiberties.length === 0 && capturedStones === 0) {// 自杀手,不允许this.board[row][col] = null;this.updateStatus('不能自杀!');return false;}// 记录这一步this.moveHistory.push({col: col,row: row,player: this.currentPlayer,board: this.board.map(row => [...row]), // 深拷贝棋盘状态captured: capturedStones});// 更新在盘棋子统计this.updateBoardStats();// 重绘棋盘this.drawBoard();// 更新统计显示this.updateStats();// 显示吃子信息if (capturedStones > 0) {this.updateStatus(`${this.currentPlayer === 'black' ? '黑棋' : '白棋'}吃掉了${capturedStones}个子!`);} else {this.updateStatus('');}// 切换玩家this.currentPlayer = this.currentPlayer === 'black' ? 'white' : 'black';this.updatePlayerDisplay();return true;}// 检查位置是否在棋盘内isValidPosition(col, row) {return col >= 0 && col < this.boardSize && row >= 0 && row < this.boardSize;}// 获取连通的棋子群getGroup(col, row) {const color = this.board[row][col];if (!color) return [];const group = [];const visited = new Set();const stack = [{ col, row }];while (stack.length > 0) {const { col: currentCol, row: currentRow } = stack.pop();const key = `${currentCol},${currentRow}`;if (visited.has(key)) continue;visited.add(key);if (this.board[currentRow][currentCol] === color) {group.push({ col: currentCol, row: currentRow });// 检查四个方向const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newCol = currentCol + dx;const newRow = currentRow + dy;const newKey = `${newCol},${newRow}`;if (this.isValidPosition(newCol, newRow) && !visited.has(newKey)) {stack.push({ col: newCol, row: newRow });}}}}return group;}// 获取棋子群的气(空位)getLiberties(group) {const liberties = new Set();for (let stone of group) {const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];for (let [dx, dy] of directions) {const newCol = stone.col + dx;const newRow = stone.row + dy;if (this.isValidPosition(newCol, newRow) &&this.board[newRow][newCol] === null) {liberties.add(`${newCol},${newRow}`);}}}return Array.from(liberties).map(key => {const [col, row] = key.split(',').map(Number);return { col, row };});}updatePlayerDisplay() {const playerElement = document.getElementById('currentPlayer');if (this.currentPlayer === 'black') {playerElement.textContent = '当前玩家: 黑棋';playerElement.className = 'current-player black-turn';} else {playerElement.textContent = '当前玩家: 白棋';playerElement.className = 'current-player white-turn';}}updateStatus(message) {document.getElementById('gameStatus').textContent = message || '点击棋盘下棋';}// 更新在盘棋子统计updateBoardStats() {let blackCount = 0;let whiteCount = 0;for (let row = 0; row < this.boardSize; row++) {for (let col = 0; col < this.boardSize; col++) {if (this.board[row][col] === 'black') {blackCount++;} else if (this.board[row][col] === 'white') {whiteCount++;}}}this.stats.blackOnBoard = blackCount;this.stats.whiteOnBoard = whiteCount;}// 更新统计显示updateStats() {document.getElementById('blackOnBoard').textContent = this.stats.blackOnBoard;document.getElementById('whiteOnBoard').textContent = this.stats.whiteOnBoard;document.getElementById('blackCaptured').textContent = this.stats.blackCaptured;document.getElementById('whiteCaptured').textContent = this.stats.whiteCaptured;}resetGame() {this.board = Array(this.boardSize).fill().map(() => Array(this.boardSize).fill(null));this.currentPlayer = 'black';this.moveHistory = [];// 重置统计this.stats = {blackCaptured: 0,whiteCaptured: 0,blackOnBoard: 0,whiteOnBoard: 0};this.drawBoard();this.updatePlayerDisplay();this.updateStats();this.updateStatus('游戏重新开始');}undoMove() {if (this.moveHistory.length === 0) {this.updateStatus('没有可以悔棋的步数');return;}// 移除最后一步this.moveHistory.pop();if (this.moveHistory.length > 0) {// 恢复到上一步的棋盘状态const lastMove = this.moveHistory[this.moveHistory.length - 1];this.board = lastMove.board.map(row => [...row]);this.currentPlayer = lastMove.player === 'black' ? 'white' : 'black';} else {// 如果没有历史记录,重置棋盘this.board = Array(this.boardSize).fill().map(() => Array(this.boardSize).fill(null));this.currentPlayer = 'black';}// 重新计算统计信息this.recalculateStats();this.drawBoard();this.updatePlayerDisplay();this.updateStats();this.updateStatus('已悔棋');}// 重新计算所有统计信息(用于悔棋后)recalculateStats() {// 重置统计this.stats.blackCaptured = 0;this.stats.whiteCaptured = 0;// 从历史记录中重新计算被吃棋子数for (let move of this.moveHistory) {if (move.captured > 0) {const opponent = move.player === 'black' ? 'white' : 'black';if (opponent === 'black') {this.stats.blackCaptured += move.captured;} else {this.stats.whiteCaptured += move.captured;}}}// 更新在盘棋子统计this.updateBoardStats();}
}// 全局函数供HTML调用
let game;function resetGame() {game.resetGame();
}function undoMove() {game.undoMove();
}// 页面加载完成后初始化游戏
window.addEventListener('load', () => {game = new WeiQiGame();
});