一、核心功能
- 双人对战:两人轮流使用同一设备,红方先行,黑方后行,完全遵循中国象棋标准规则。
- 人机对战:可选择让 AI 执红或执黑,自己执另一方,与电脑对弈。
- 三种难度:初级(1层搜索深度)、中级(2层)、高级(3层),层数越高 AI 思考更深入,棋力越强。
- 悔棋功能:可回退到上一回合的局面,方便复盘或纠正失误。
- 新局重开:随时重置棋盘,开始全新对局。
二、界面设计
- 棋盘绘制:采用传统木质色调,楚河汉界清晰,九宫对角线、炮/兵标记点均完整呈现。
- 棋子样式:红方棋子为暗红色底、金色文字;黑方为深色底、浅色文字,文字采用楷体,带有立体阴影,视觉上非常典雅。
- 选中高亮:当点击己方棋子时,棋子周围会出现金色光圈,提示当前选中状态。
- 右侧面板:
- 走棋提示:显示当前轮到红方还是黑方走棋,对局结束后显示胜方。
- 模式选择:下拉菜单可选"双人对战"、"AI执黑"、"AI执红"。
- 难度选择:初级、中级、高级对应不同的搜索深度。
- 悔棋按钮:点击后退一步。
- 新局按钮:立即重置棋盘。
三、规则实现游戏严格实现了中国象棋的全部基本规则:
- 棋子走法:
- 帅(将):只在九宫内直走一步。
- 士(仕):只在九宫内斜走一步。
- 相(象):飞"田",不能过河,且需避免象眼被塞。
- 马:走"日"字,有蹩马腿限制。
- 车:直线任意距离,不可越过棋子。
- 炮:直线行走,吃子时必须隔一棋子(炮架)。
- 兵(卒):未过河只能前进,过河后可横移,不可后退。
- 特殊规则:
- 将帅不能照面(双方将/帅之间无棋子阻挡时判为禁止走法)。
- 走棋后不能使己方将/帅处于被攻击状态(即不能送将)。
- 胜负判定:吃掉对方将/帅即获胜。程序会在每次移动后检测对方将/帅是否存在。
四、AI 设计
- 搜索算法:采用带 Alpha-Beta 剪枝的博弈树搜索,深度由用户选择(1~3层)。
- 优化技巧:
- 杀手启发式:记录每层搜索中导致剪枝的非吃子走法,优先尝试这些走法以提高剪枝效率。
- 走法排序:先尝试杀手走法,再按吃子价值(MVV-LVA)排序,提高剪枝效果。
- 局面评估:基于棋子基本价值(帅10000、车1000、马500、炮800、相/士200、兵/卒100),计算双方总分差。
- 随机性:当多个走法评分相同时,随机选择一个,使 AI 棋路略有变化。
五、操作方式
- 选择棋子:点击棋盘上的己方棋子,该棋子会高亮。
- 移动棋子:再点击一个合法的目标格,棋子即移动。若目标格有敌方棋子则吃掉。
- 取消选择:点击空白区域或再次点击同一棋子可取消选中。
- 按钮操作:点击右侧按钮执行对应功能,所有操作均有实时反馈。
六、技术实现亮点
- Canvas 锐化处理:所有线条和文字均采用半像素坐标绘制,消除模糊,在视网膜屏幕下依然清晰。
- 响应式布局:棋盘尺寸固定,但通过 CSS 居中显示,适配不同分辨率。
- 无外部依赖:纯原生 JavaScript,无任何第三方库,加载即用。
- 历史记录:每次走棋都会保存局面,实现悔棋功能。
七、适用场景
-
闲暇时与朋友对弈一局。
-
初学者练习走法,熟悉规则。
-
象棋爱好者与 AI 切磋,逐步挑战更高难度。
-
无需网络,随时在电脑或平板浏览器上打开即玩。
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>中国象棋专业版2.0</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; } body { background: #1a1a1a; min-height: 100vh; display: flex; justify-content: center; align-items: center; font-family: '楷体', 'KaiTi', '华文楷体', serif; } .game-container { background: transparent; padding: 20px; border-radius: 0; box-shadow: none; border: none; display: flex; gap: 40px; align-items: center; } /* 强制canvas显示尺寸与像素尺寸一致,防止浏览器缩放导致模糊 */ canvas { display: block; width: 560px; height: 630px; border-radius: 18px; box-shadow: 0 10px 20px rgba(0,0,0,0.3); background: #f0d9b5; cursor: pointer; } .right-panel { display: flex; flex-direction: column; gap: 15px; align-items: center; justify-content: center; min-width: 160px; } .turn-indicator { background: #5e3e1e; color: #f9eac3; padding: 8px 12px; border-radius: 50px; font-size: 22px; letter-spacing: 2px; box-shadow: inset 0 -3px 0 #2f1f0e, 0 6px 10px black; border-bottom: 2px solid #b5914c; width: 100%; text-align: center; margin-bottom: 5px; } .mode-selector, .difficulty-selector { background: #c29e5e; border-radius: 40px; padding: 8px 12px; font-size: 20px; font-weight: bold; color: #2b1a0a; border: 2px solid #efd39e; box-shadow: inset 0 -2px 0 #6f4f2e, 0 6px 8px black; cursor: pointer; outline: none; width: 100%; text-align: center; } .action-btn { background: #c29e5e; border: none; font-size: 22px; font-family: '楷体', serif; font-weight: bold; padding: 8px 0; border-radius: 40px; color: #2b1a0a; box-shadow: 0 8px 0 #6f4f2e, 0 10px 15px black; cursor: pointer; transition: 0.08s linear; border: 1px solid #efd39e; letter-spacing: 2px; width: 100%; } .action-btn:active { transform: translateY(7px); box-shadow: 0 2px 0 #5a3f22, 0 8px 12px black; } </style> </head> <body><canvas id="chessCanvas" width="560" height="630"></canvas>红方走棋<select id="modeSelect" class="mode-selector"> <option value="human" selected>双人对战</option> <option value="ai-black">🤖AI执黑</option> <option value="ai-red">🤖AI执红</option> </select> <select id="difficultySelect" class="difficulty-selector"> <option value="1">🤖初级1层</option> <option value="2" selected>🤖中级2层</option> <option value="3">🤖高级3层</option> </select> <button class="action-btn" id="undoBtn">悔棋</button> <button class="action-btn restart-btn" id="restartBtn">新局</button></body> </html><script> (function() { // ---------- 棋盘配置 ---------- const canvas = document.getElementById('chessCanvas'); const ctx = canvas.getContext('2d'); const turnText = document.getElementById('turnText'); const modeSelect = document.getElementById('modeSelect'); const difficultySelect = document.getElementById('difficultySelect'); const undoBtn = document.getElementById('undoBtn'); const restartBtn = document.getElementById('restartBtn'); const ROWS = 10; const COLS = 9; const GRID_SIZE = 56; const OFFSET_X = 56; const OFFSET_Y = 63; // ---------- 全局状态 ---------- let board = []; let currentPlayer = 'r'; let selectedPiece = null; let gameOver = false; let winner = null; let history = []; // AI 相关 let gameMode = 'human'; let aiDifficulty = 2; let isAIPlaying = false; let killerMoves = []; const MAX_DEPTH = 10; function initKillers() { killerMoves = []; for (let d = 0; d <= MAX_DEPTH; d++) killerMoves[d] = [null, null]; } initKillers(); const pieceValue = { 'k': 10000, 'a': 200, 'b': 200, 'n': 500, 'r': 1000, 'c': 800, 'p': 100 }; function initBoard() { board = Array(ROWS).fill().map(() => Array(COLS).fill(null)); board[0][0] = 'br'; board[0][1] = 'bn'; board[0][2] = 'bb'; board[0][3] = 'ba'; board[0][4] = 'bk'; board[0][5] = 'ba'; board[0][6] = 'bb'; board[0][7] = 'bn'; board[0][8] = 'br'; board[2][1] = 'bc'; board[2][7] = 'bc'; board[3][0] = 'bp'; board[3][2] = 'bp'; board[3][4] = 'bp'; board[3][6] = 'bp'; board[3][8] = 'bp'; board[9][0] = 'rr'; board[9][1] = 'rn'; board[9][2] = 'rb'; board[9][3] = 'ra'; board[9][4] = 'rk'; board[9][5] = 'ra'; board[9][6] = 'rb'; board[9][7] = 'rn'; board[9][8] = 'rr'; board[7][1] = 'rc'; board[7][7] = 'rc'; board[6][0] = 'rp'; board[6][2] = 'rp'; board[6][4] = 'rp'; board[6][6] = 'rp'; board[6][8] = 'rp'; currentPlayer = 'r'; selectedPiece = null; gameOver = false; winner = null; history = []; turnText.innerText = '红方走棋'; initKillers(); } // ---------- 绘制函数 (锐化处理) ---------- function drawBoard() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawGrid(); drawPieces(); if (selectedPiece) { const { row, col } = selectedPiece; // 选中高亮:坐标对齐到半像素,阴影调小 ctx.save(); ctx.strokeStyle = '#f5e56b'; ctx.lineWidth = 3; ctx.shadowBlur = 6; // 减小阴影模糊 ctx.shadowColor = 'gold'; ctx.beginPath(); // 使用半像素坐标使圆弧边缘更清晰 ctx.arc(OFFSET_X + col * GRID_SIZE + 0.5, OFFSET_Y + row * GRID_SIZE + 0.5, 24, 0, 2 * Math.PI); ctx.stroke(); ctx.restore(); } } function drawGrid() { ctx.save(); ctx.strokeStyle = '#6b4f32'; ctx.lineWidth = 1; // 线宽设为1,配合半像素坐标获得锐利直线 // 绘制横线 (坐标+0.5使线条精确覆盖像素) for (let i = 0; i < ROWS; i++) { ctx.beginPath(); ctx.moveTo(OFFSET_X + 0.5, OFFSET_Y + i * GRID_SIZE + 0.5); ctx.lineTo(OFFSET_X + (COLS - 1) * GRID_SIZE + 0.5, OFFSET_Y + i * GRID_SIZE + 0.5); ctx.stroke(); } // 绘制竖线 for (let i = 0; i < COLS; i++) { ctx.beginPath(); ctx.moveTo(OFFSET_X + i * GRID_SIZE + 0.5, OFFSET_Y + 0.5); ctx.lineTo(OFFSET_X + i * GRID_SIZE + 0.5, OFFSET_Y + (ROWS - 1) * GRID_SIZE + 0.5); ctx.stroke(); } // 绘制楚河汉界 (半透明背景文字,稍微下移一点避开中线,保留原风格) ctx.font = '35px "楷体", "KaiTi"'; ctx.fillStyle = '#4e2f15'; ctx.globalAlpha = 0.3; const text = '楚 河 汉 界'; const textWidth = ctx.measureText(text).width; const boardLeft = OFFSET_X; const boardRight = OFFSET_X + (COLS - 1) * GRID_SIZE; const centerX = (boardLeft + boardRight) / 2; const textX = centerX - textWidth / 2; // 保持文字整数坐标防止模糊 (半透明背景无需锐利) ctx.fillText(text, textX, OFFSET_Y + 5 * GRID_SIZE - 13); ctx.globalAlpha = 1; // 九宫对角线 (同样用半像素坐标) ctx.beginPath(); ctx.moveTo(OFFSET_X + 3 * GRID_SIZE + 0.5, OFFSET_Y + 0.5); ctx.lineTo(OFFSET_X + 5 * GRID_SIZE + 0.5, OFFSET_Y + 2 * GRID_SIZE + 0.5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(OFFSET_X + 5 * GRID_SIZE + 0.5, OFFSET_Y + 0.5); ctx.lineTo(OFFSET_X + 3 * GRID_SIZE + 0.5, OFFSET_Y + 2 * GRID_SIZE + 0.5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(OFFSET_X + 3 * GRID_SIZE + 0.5, OFFSET_Y + 7 * GRID_SIZE + 0.5); ctx.lineTo(OFFSET_X + 5 * GRID_SIZE + 0.5, OFFSET_Y + 9 * GRID_SIZE + 0.5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(OFFSET_X + 5 * GRID_SIZE + 0.5, OFFSET_Y + 7 * GRID_SIZE + 0.5); ctx.lineTo(OFFSET_X + 3 * GRID_SIZE + 0.5, OFFSET_Y + 9 * GRID_SIZE + 0.5); ctx.stroke(); // 绘制炮/兵/卒标记点 (使用半像素坐标,使圆点清晰) ctx.fillStyle = '#6b4f32'; ctx.shadowBlur = 0; // 标记点不要阴影 for (let r of [2, 7]) { for (let c of [1, 7]) { ctx.beginPath(); ctx.arc(OFFSET_X + c * GRID_SIZE + 0.5, OFFSET_Y + r * GRID_SIZE + 0.5, 5, 0, 2 * Math.PI); ctx.fill(); } } for (let r of [3, 6]) { for (let c of [0, 2, 4, 6, 8]) { ctx.beginPath(); ctx.arc(OFFSET_X + c * GRID_SIZE + 0.5, OFFSET_Y + r * GRID_SIZE + 0.5, 4, 0, 2 * Math.PI); ctx.fill(); } } ctx.restore(); } function drawPieces() { for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const piece = board[r][c]; if (!piece) continue; // 圆心坐标对齐半像素,使棋子边缘清晰 const x = OFFSET_X + c * GRID_SIZE + 0.5; const y = OFFSET_Y + r * GRID_SIZE + 0.5; ctx.save(); // 棋子阴影减弱 ctx.shadowColor = '#333'; ctx.shadowBlur = 4; // 原为8,减半减少模糊 ctx.shadowOffsetY = 2; // 原为3 ctx.beginPath(); ctx.arc(x, y, 24, 0, 2 * Math.PI); ctx.fillStyle = piece[0] === 'r' ? '#c33' : '#222'; ctx.fill(); ctx.shadowBlur = 2; // 描边阴影稍弱 ctx.strokeStyle = '#f1d28c'; ctx.lineWidth = 2; ctx.stroke(); // 绘制文字前清除阴影,保证文字清晰 ctx.shadowBlur = 0; ctx.shadowOffsetY = 0; ctx.font = '35px "楷体", "KaiTi", "华文楷体", serif'; ctx.fillStyle = piece[0] === 'r' ? '#FFD966' : '#ddd'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // 微调偏移量,配合半像素圆心,文字仍保留轻微偏移以居中 ctx.fillText(pieceToChinese(piece), x - 1, y + 5); ctx.restore(); } } } function pieceToChinese(p) { const map = { 'r': { 'k': '帅', 'a': '仕', 'b': '相', 'n': '马', 'r': '车', 'c': '炮', 'p': '兵' }, 'b': { 'k': '将', 'a': '士', 'b': '象', 'n': '马', 'r': '车', 'c': '炮', 'p': '卒' } }; return map[p[0]][p[1]] || '?'; } // ---------- 规则核心函数 (保持不变,保证AI逻辑正确) ---------- function getPieceRawMoves(boardState, row, col, piece) { const color = piece[0]; const type = piece[1]; const moves = []; if (type === 'k') { const palaceRows = (color === 'r') ? [7,8,9] : [0,1,2]; const palaceCols = [3,4,5]; const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; for (let [dx,dy] of dirs) { const nr = row+dx, nc = col+dy; if (palaceRows.includes(nr) && palaceCols.includes(nc)) { const target = boardState[nr]?.[nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } } } else if (type === 'a') { const palaceRows = (color === 'r') ? [7,8,9] : [0,1,2]; const palaceCols = [3,4,5]; const dirs = [[-1,-1],[-1,1],[1,-1],[1,1]]; for (let [dx,dy] of dirs) { const nr = row+dx, nc = col+dy; if (palaceRows.includes(nr) && palaceCols.includes(nc)) { const target = boardState[nr]?.[nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } } } else if (type === 'b') { const dirs = [[-2,-2],[-2,2],[2,-2],[2,2]]; for (let [dx,dy] of dirs) { const nr = row+dx, nc = col+dy; if (nr<0 || nr>=ROWS || nc<0 || nc>=COLS) continue; if (color === 'r' && nr < 5) continue; if (color === 'b' && nr > 4) continue; const eyeRow = row + dx/2, eyeCol = col + dy/2; if (boardState[eyeRow][eyeCol] !== null) continue; const target = boardState[nr][nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } } else if (type === 'n') { const jumps = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]; for (let [dx,dy] of jumps) { const nr = row+dx, nc = col+dy; if (nr<0 || nr>=ROWS || nc<0 || nc>=COLS) continue; let blockRow = row, blockCol = col; if (Math.abs(dx) === 2) { blockRow = row + (dx>0?1:-1); blockCol = col; } else { blockRow = row; blockCol = col + (dy>0?1:-1); } if (boardState[blockRow][blockCol] !== null) continue; const target = boardState[nr][nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } } else if (type === 'r') { const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; for (let [dx,dy] of dirs) { let nr = row+dx, nc = col+dy; while (nr>=0 && nr<ROWS && nc>=0 && nc<COLS) { const target = boardState[nr][nc]; if (!target) moves.push({toX:nr, toY:nc}); else { if (target[0] !== color) moves.push({toX:nr, toY:nc}); break; } nr += dx; nc += dy; } } } else if (type === 'c') { const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; for (let [dx,dy] of dirs) { let nr = row+dx, nc = col+dy; let foundObstacle = false; while (nr>=0 && nr<ROWS && nc>=0 && nc<COLS) { const target = boardState[nr][nc]; if (!foundObstacle) { if (!target) moves.push({toX:nr, toY:nc}); else foundObstacle = true; } else { if (target && target[0] !== color) { moves.push({toX:nr, toY:nc}); break; } else if (target) break; } nr += dx; nc += dy; } } } else if (type === 'p') { const forward = (color === 'r') ? -1 : 1; let nr = row + forward, nc = col; if (nr>=0 && nr<ROWS) { const target = boardState[nr][nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } const crossed = (color === 'r') ? row <= 4 : row >= 5; if (crossed) { for (let dc of [-1,1]) { nr = row; nc = col + dc; if (nc>=0 && nc<COLS) { const target = boardState[nr][nc]; if (!target || target[0] !== color) moves.push({toX:nr, toY:nc}); } } } } return moves; } function applyMove(boardState, fromX, fromY, toX, toY) { const newBoard = boardState.map(row => row.map(cell => cell)); const piece = newBoard[fromX][fromY]; newBoard[toX][toY] = piece; newBoard[fromX][fromY] = null; return newBoard; } function isKingInCheck(boardState, color) { let kingPos = null; for (let r=0; r<ROWS; r++) { for (let c=0; c<COLS; c++) { const p = boardState[r][c]; if (p && p[0]===color && p[1]==='k') kingPos = [r,c]; } } if (!kingPos) return false; const opponent = color==='r' ? 'b' : 'r'; for (let r=0; r<ROWS; r++) { for (let c=0; c<COLS; c++) { const p = boardState[r][c]; if (p && p[0]===opponent) { const rawMoves = getPieceRawMoves(boardState, r, c, p); for (let m of rawMoves) { if (m.toX === kingPos[0] && m.toY === kingPos[1]) return true; } } } } return false; } function hasGeneralsFacing(boardState) { let redKing = null, blackKing = null; for (let r=0; r<ROWS; r++) { for (let c=0; c<COLS; c++) { const p = boardState[r][c]; if (p && p[1]==='k') { if (p[0]==='r') redKing = [r,c]; else blackKing = [r,c]; } } } if (!redKing || !blackKing) return false; if (redKing[1] !== blackKing[1]) return false; const col = redKing[1]; const minRow = Math.min(redKing[0], blackKing[0]); const maxRow = Math.max(redKing[0], blackKing[0]); for (let r = minRow+1; r < maxRow; r++) { if (boardState[r][col] !== null) return false; } return true; } function getAllValidMoves(boardState, color) { const moves = []; for (let r=0; r<ROWS; r++) { for (let c=0; c<COLS; c++) { const piece = boardState[r][c]; if (!piece || piece[0] !== color) continue; const raw = getPieceRawMoves(boardState, r, c, piece); for (let m of raw) { const newBoard = applyMove(boardState, r, c, m.toX, m.toY); if (isKingInCheck(newBoard, color)) continue; if (hasGeneralsFacing(newBoard)) continue; const pieceVal = pieceValue[piece[1]] || 0; const target = boardState[m.toX][m.toY]; const targetVal = target ? (pieceValue[target[1]] || 0) : 0; moves.push({ fromX: r, fromY: c, toX: m.toX, toY: m.toY, pieceValue: pieceVal, targetValue: targetVal }); } } } return moves; } function evaluateBoard(boardState, perspective) { let score = 0; for (let r=0; r<ROWS; r++) { for (let c=0; c<COLS; c++) { const p = boardState[r][c]; if (!p) continue; const val = pieceValue[p[1]] || 0; if (p[0] === perspective) score += val; else score -= val; } } return score; } function alphaBeta(boardState, depth, alpha, beta, maximizing, aiColor, currentDepth) { const currentColor = maximizing ? aiColor : (aiColor==='r' ? 'b' : 'r'); const moves = getAllValidMoves(boardState, currentColor); if (moves.length === 0) { return maximizing ? -Infinity : Infinity; } if (depth === 0) return evaluateBoard(boardState, aiColor); const killers = killerMoves[currentDepth] || [null, null]; moves.sort((a,b) => { const aKiller = (killers[0] && a.fromX===killers[0].fromX && a.fromY===killers[0].fromY && a.toX===killers[0].toX && a.toY===killers[0].toY) || (killers[1] && a.fromX===killers[1].fromX && a.fromY===killers[1].fromY && a.toX===killers[1].toX && a.toY===killers[1].toY); const bKiller = (killers[0] && b.fromX===killers[0].fromX && b.fromY===killers[0].fromY && b.toX===killers[0].toX && b.toY===killers[0].toY) || (killers[1] && b.fromX===killers[1].fromX && b.fromY===killers[1].fromY && b.toX===killers[1].toX && b.toY===killers[1].toY); if (aKiller && !bKiller) return -1; if (!aKiller && bKiller) return 1; if (a.targetValue !== b.targetValue) return b.targetValue - a.targetValue; return b.pieceValue - a.pieceValue; }); if (maximizing) { let maxEval = -Infinity; for (let move of moves) { const newBoard = applyMove(boardState, move.fromX, move.fromY, move.toX, move.toY); const evalScore = alphaBeta(newBoard, depth-1, alpha, beta, false, aiColor, currentDepth+1); maxEval = Math.max(maxEval, evalScore); alpha = Math.max(alpha, evalScore); if (beta <= alpha) { if (move.targetValue === 0) { if (!killerMoves[currentDepth]) killerMoves[currentDepth] = [null, null]; const k = killerMoves[currentDepth]; if (!k[0]) k[0] = move; else if (!k[1] && !(k[0].fromX===move.fromX && k[0].fromY===move.fromY && k[0].toX===move.toX && k[0].toY===move.toY)) k[1] = move; } break; } } return maxEval; } else { let minEval = Infinity; for (let move of moves) { const newBoard = applyMove(boardState, move.fromX, move.fromY, move.toX, move.toY); const evalScore = alphaBeta(newBoard, depth-1, alpha, beta, true, aiColor, currentDepth+1); minEval = Math.min(minEval, evalScore); beta = Math.min(beta, evalScore); if (beta <= alpha) { if (move.targetValue === 0) { if (!killerMoves[currentDepth]) killerMoves[currentDepth] = [null, null]; const k = killerMoves[currentDepth]; if (!k[0]) k[0] = move; else if (!k[1] && !(k[0].fromX===move.fromX && k[0].fromY===move.fromY && k[0].toX===move.toX && k[0].toY===move.toY)) k[1] = move; } break; } } return minEval; } } function getBestMove(boardState, color, depth) { initKillers(); const moves = getAllValidMoves(boardState, color); if (moves.length === 0) return null; let bestMoves = []; let bestScore = -Infinity; for (let move of moves) { const newBoard = applyMove(boardState, move.fromX, move.fromY, move.toX, move.toY); const score = alphaBeta(newBoard, depth-1, -Infinity, Infinity, false, color, 0); if (score > bestScore) { bestScore = score; bestMoves = [move]; } else if (score === bestScore) { bestMoves.push(move); } } return bestMoves[Math.floor(Math.random() * bestMoves.length)]; } function aiMakeMove(color) { if (gameOver || isAIPlaying) return; const isAITurn = (gameMode === 'ai-black' && color === 'b') || (gameMode === 'ai-red' && color === 'r'); if (!isAITurn) return; isAIPlaying = true; setTimeout(() => { if (gameOver) { isAIPlaying = false; return; } const depth = aiDifficulty; const bestMove = getBestMove(board, color, depth); if (!bestMove) { gameOver = true; winner = (color === 'r') ? 'b' : 'r'; turnText.innerText = winner === 'r' ? '🔴 红方胜' : '⚫ 黑方胜'; drawBoard(); isAIPlaying = false; return; } const { fromX, fromY, toX, toY } = bestMove; const boardCopy = board.map(row => row.map(cell => cell)); history.push({ board: boardCopy, currentPlayer, lastMoveFrom: [fromX,fromY], lastMoveTo: [toX,toY] }); const piece = board[fromX][fromY]; board[toX][toY] = piece; board[fromX][fromY] = null; const enemyColor = color === 'r' ? 'b' : 'r'; let enemyKingAlive = false; for (let r=0; r<ROWS; r++) for (let c=0; c<COLS; c++) { const p = board[r][c]; if (p && p[0]===enemyColor && p[1]==='k') enemyKingAlive = true; } if (!enemyKingAlive) { gameOver = true; winner = color; turnText.innerText = winner === 'r' ? '红方胜' : '黑方胜'; } else { currentPlayer = enemyColor; turnText.innerText = currentPlayer === 'r' ? '红方走棋' : '黑方走棋'; } selectedPiece = null; drawBoard(); if (!gameOver) { const nextIsAI = (gameMode === 'ai-black' && currentPlayer === 'b') || (gameMode === 'ai-red' && currentPlayer === 'r'); if (nextIsAI) { setTimeout(() => { aiMakeMove(currentPlayer); }, 150); } } isAIPlaying = false; }, 150); } function handleCanvasClick(e) { if (gameOver) return; const isCurrentAITurn = (gameMode === 'ai-black' && currentPlayer === 'b') || (gameMode === 'ai-red' && currentPlayer === 'r'); if (isCurrentAITurn) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const canvasX = (e.clientX - rect.left) * scaleX; const canvasY = (e.clientY - rect.top) * scaleY; const col = Math.round((canvasX - OFFSET_X) / GRID_SIZE); const row = Math.round((canvasY - OFFSET_Y) / GRID_SIZE); if (row<0 || row>=ROWS || col<0 || col>=COLS) return; if (!selectedPiece) { const piece = board[row][col]; if (piece && piece[0] === currentPlayer) { selectedPiece = { row, col, piece }; } } else { const { row: sRow, col: sCol, piece: sPiece } = selectedPiece; if (board[row][col] && board[row][col][0] === currentPlayer) { selectedPiece = { row, col, piece: board[row][col] }; drawBoard(); return; } const rawMoves = getPieceRawMoves(board, sRow, sCol, sPiece); let valid = false; for (let m of rawMoves) { if (m.toX === row && m.toY === col) { const newBoard = applyMove(board, sRow, sCol, row, col); if (!isKingInCheck(newBoard, currentPlayer) && !hasGeneralsFacing(newBoard)) { valid = true; } break; } } if (valid) { const boardCopy = board.map(r => r.map(c => c)); history.push({ board: boardCopy, currentPlayer, lastMoveFrom: [sRow,sCol], lastMoveTo: [row,col] }); const piece = board[sRow][sCol]; board[row][col] = piece; board[sRow][sCol] = null; const enemyColor = currentPlayer === 'r' ? 'b' : 'r'; let enemyKingAlive = false; for (let r=0; r<ROWS; r++) for (let c=0; c<COLS; c++) { const p = board[r][c]; if (p && p[0]===enemyColor && p[1]==='k') enemyKingAlive = true; } if (!enemyKingAlive) { gameOver = true; winner = currentPlayer; turnText.innerText = winner === 'r' ? '红方胜' : '黑方胜'; } else { currentPlayer = enemyColor; turnText.innerText = currentPlayer === 'r' ? '红方走棋' : '黑方走棋'; } selectedPiece = null; drawBoard(); if (!gameOver) { const nextIsAI = (gameMode === 'ai-black' && currentPlayer === 'b') || (gameMode === 'ai-red' && currentPlayer === 'r'); if (nextIsAI) { setTimeout(() => { aiMakeMove(currentPlayer); }, 200); } } } else { selectedPiece = null; } } drawBoard(); } function undoMove() { if (history.length === 0) return; if (isAIPlaying) return; const last = history.pop(); board = last.board.map(row => row.map(cell => cell)); currentPlayer = last.currentPlayer; selectedPiece = null; gameOver = false; winner = null; turnText.innerText = currentPlayer === 'r' ? '红方走棋' : '黑方走棋'; drawBoard(); if (!gameOver) { const nextIsAI = (gameMode === 'ai-black' && currentPlayer === 'b') || (gameMode === 'ai-red' && currentPlayer === 'r'); if (nextIsAI) { setTimeout(() => { aiMakeMove(currentPlayer); }, 200); } } } function restartGame() { if (isAIPlaying) return; initBoard(); drawBoard(); if (gameMode === 'ai-black' && currentPlayer === 'b') aiMakeMove('b'); else if (gameMode === 'ai-red' && currentPlayer === 'r') aiMakeMove('r'); } canvas.addEventListener('click', handleCanvasClick); undoBtn.addEventListener('click', undoMove); restartBtn.addEventListener('click', restartGame); modeSelect.addEventListener('change', (e) => { gameMode = e.target.value; restartGame(); }); difficultySelect.addEventListener('change', (e) => { aiDifficulty = parseInt(e.target.value, 10); }); initBoard(); drawBoard(); })(); </script>
