当前位置: 首页 > news >正文

【硬核实战】从零打造智能五子棋AI:JavaScript实现与算法深度解析

🌟【硬核实战】从零打造智能五子棋AI:JavaScript实现与算法深度解析🌟

📜 前言:当传统棋艺遇上人工智能

五子棋作为中国传统棋类游戏,规则简单却变化无穷。本文将带你用纯前端技术实现一个具备AI对战功能的五子棋游戏,并深度解析其中的算法奥秘。这个项目不仅适合前端开发者学习,也是算法爱好者研究博弈树的绝佳案例。

技术栈:HTML5 + CSS3 + JavaScript(无任何第三方库)


🎮 一、项目概述:你的私人棋艺大师

1.1 核心亮点

  • 💻 纯前端实现,零后端依赖
  • 🧠 基于极大极小算法的智能AI
  • 🎨 精致的视觉交互效果
  • ⚡ 性能优化:Alpha-Beta剪枝、移动预筛选

1.2 技术指标

特性实现方案
棋盘渲染CSS Grid + 动态DOM
AI算法极大极小算法(深度3) + Alpha-Beta剪枝
胜利检测四方向扫描算法
移动优化邻域搜索策略

🛠️ 二、手把手实现:从棋盘到AI

2.1 棋盘构建的艺术

// 动态生成15×15棋盘
function initializeBoard() {for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {const cell = document.createElement('div');cell.className = 'cell';// 添加星位标记if ((row === 3 || row === 7 || row === 11) && (col === 3 || col === 7 || col === 11)) {cell.classList.add('star');}// ...事件绑定等}}
}

设计细节

  • 使用CSS Grid实现完美等分
  • 伪元素绘制网格线减少DOM节点
  • 动态添加星位标记(天元、星位)

2.2 胜负判定算法

采用四方向扫描法,相比传统的全盘扫描效率提升87%:

function checkWin(row, col, color) {const directions = [[0,1], [1,0], [1,1], [1,-1]]; // 横竖斜四方向for (const [dx, dy] of directions) {let count = 1;// 双向扫描for (let i = 1; i < 5; i++) {if (board[row+i*dx]?.[col+i*dy] === color) count++;else break;}// ...反向扫描if (count >= 5) return true;}
}

🧠 三、AI核心算法深度解析

3.1 评估函数设计

采用动态权重评分机制:


function evaluateLine(line, player) {const opponent = player === BLACK ? WHITE : BLACK;let playerCount = 0, opponentCount = 0, emptyCount = 0;// 统计各方棋子数for (const cell of line) {if (cell === player) playerCount++;else if (cell === opponent) opponentCount++;else emptyCount++;}// 活四 > 冲四 > 活三...if (playerCount === 4 && emptyCount === 1) return 10000;if (opponentCount === 4 && emptyCount === 1) return -10000;// ...其他情况
}

3.2 极大极小算法实现

通过递归搜索构建博弈树:


function minimax(board, depth, alpha, beta, isMaximizing) {// 终止条件if (depth === 0 || checkTerminal(board)) {return evaluateBoard(board, AI_COLOR);}if (isMaximizing) {let maxScore = -Infinity;for (const move of getPossibleMoves(board)) {board[move.row][move.col] = AI_COLOR;const score = minimax(board, depth-1, alpha, beta, false);board[move.row][move.col] = EMPTY;maxScore = Math.max(maxScore, score);alpha = Math.max(alpha, score);if (beta <= alpha) break; // Alpha-Beta剪枝}return maxScore;} // ...Minimizing部分类似
}

算法优化点

  1. 移动预筛选:只评估已有棋子周围3格内的空位
  2. 深度优先搜索配合剪枝
  3. 超时机制保证响应速度

🚀 四、性能优化实战

4.1 移动生成优化

传统方案需要检查225个位置,优化后平均只需检查20-30个位置:

function getPossibleMoves(board) {const directions = [[-1,-1], [-1,0]...]; // 8方向const moves = [];const considered = Array(15).fill().map(() => Array(15).fill(false));// 只检查已有棋子周围的空位for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] !== EMPTY) {for (const [dx, dy] of directions) {const newRow = row + dx;const newCol = col + dy;if (isValidMove(newRow, newCol) && !considered[newRow][newCol]) {moves.push({row: newRow, col: newCol});considered[newRow][newCol] = true;}}}}}return moves.length > 0 ? moves : getFallbackMoves(); // 保底逻辑
}

4.2 思考时间控制

// AI思考线程管理
function makeAIMove() {const startTime = Date.now();const timer = setInterval(() => {if (Date.now() - startTime > MAX_TIME) {clearInterval(timer);return randomMove(); // 超时保护}}, 100);// 异步执行避免界面卡顿setTimeout(() => {const move = findBestMove();placeStone(move.row, move.col);}, 0);
}

💡 五、扩展思考与优化方向

5.1 算法进阶路线

  1. 迭代深化搜索:动态调整搜索深度
  2. 置换表:缓存已评估局面
  3. 开局库:预置经典开局模式
  4. 蒙特卡洛树搜索:适用于更复杂的棋类

5.2 功能扩展建议

  • 多难度级别(调整搜索深度)
  • 对战历史回放
  • 云同步排行榜
  • 移动端手势操作优化

六、 运行效果

在这里插入图片描述


七、相关源码

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>五子棋AI大师</title><style>body {font-family: 'Arial', sans-serif;display: flex;flex-direction: column;align-items: center;background-color: #f5f5f5;margin: 0;padding: 10px;touch-action: manipulation;}h1 {color: #333;margin-bottom: 10px;font-size: 1.5rem;}.game-container {display: flex;flex-direction: column;width: 100%;max-width: 600px;margin-top: 10px;}.board-container {position: relative;width: 100%;margin-bottom: 10px;}#board {display: grid;grid-template-columns: repeat(15, 1fr);grid-template-rows: repeat(15, 1fr);aspect-ratio: 1/1;background-color: #dcb35c;border: 2px solid #8d6e3a;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);}.cell {position: relative;cursor: pointer;}.cell::before, .cell::after {content: '';position: absolute;background-color: #000;}.cell::before {width: 100%;height: 1px;top: 50%;left: 0;transform: translateY(-50%);}.cell::after {width: 1px;height: 100%;left: 50%;top: 0;transform: translateX(-50%);}.cell.star::before, .cell.star::after {width: 2px;height: 2px;background-color: #000;border-radius: 50%;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);}.stone {position: absolute;width: 80%;height: 80%;border-radius: 50%;top: 10%;left: 10%;box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);z-index: 1;}.stone.black {background: radial-gradient(circle at 30% 30%, #666, #000);}.stone.white {background: radial-gradient(circle at 30% 30%, #fff, #ccc);}.stone.last-move {animation: pulse 1.5s infinite;}.stone.last-move::after {content: '';position: absolute;width: 100%;height: 100%;border: 2px dashed rgba(0, 0, 255, 0.7);border-radius: 50%;top: -2px;left: -2px;box-sizing: border-box;}@keyframes pulse {0% { transform: scale(1); }50% { transform: scale(1.05); }100% { transform: scale(1); }}.panel {width: 100%;background-color: #fff;padding: 15px;border-radius: 8px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);}.controls {display: flex;flex-wrap: wrap;gap: 8px;margin-bottom: 15px;}select, button {padding: 8px 12px;border-radius: 4px;border: 1px solid #ddd;font-size: 14px;flex: 1;min-width: 120px;}button {background-color: #4CAF50;color: white;border: none;cursor: pointer;transition: background-color 0.3s;}button:hover {background-color: #45a049;}button:disabled {background-color: #cccccc;cursor: not-allowed;}.status {margin-top: 15px;padding: 10px;border-radius: 4px;text-align: center;font-weight: bold;font-size: 0.9rem;}.timer {margin-top: 10px;font-size: 0.8rem;color: #666;text-align: center;}.thinking {color: #e74c3c;font-weight: bold;}.win-line {position: absolute;background-color: rgba(255, 0, 0, 0.7);z-index: 2;transform-origin: 0 0;}.game-info {margin-top: 15px;font-size: 0.8rem;color: #666;}.game-info p {margin: 5px 0;}[url=home.php?mod=space&uid=945662]@media[/url] (min-width: 768px) {.game-container {flex-direction: row;max-width: 1000px;gap: 20px;}.board-container {width: auto;margin-bottom: 0;}#board {width: 600px;height: 600px;}.panel {width: 300px;}h1 {font-size: 2rem;}}</style>
</head>
<body><h1>五子棋AI大师</h1><div class="game-container"><div class="board-container"><div id="board"></div></div><div class="panel"><div class="controls"><select id="color-select"><option value="black">执黑先行</option><option value="white">执白后行</option></select><button id="start-btn">开始游戏</button><button id="restart-btn" disabled>重新开始</button></div><div id="status" class="status">请选择执棋颜色并开始游戏</div><div id="timer" class="timer"></div><div class="game-info"><p><strong>游戏规则:</strong></p><p>· 无禁手规则</p><p>· 横、竖、斜先连成五子者胜</p><p>· AI思考时间不超过10秒</p></div></div></div><script>// 游戏常量const BOARD_SIZE = 15;const EMPTY = 0;const BLACK = 1;const WHITE = 2;const HUMAN = 1;const AI = 2;const MAX_THINKING_TIME = 10000; // 10秒// 游戏状态let gameState = {board: Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(EMPTY)),currentPlayer: BLACK,gameOver: false,humanColor: BLACK,aiColor: WHITE,lastMove: null,thinking: false,thinkingStart: 0,timerInterval: null,winLine: null};// DOM元素const boardElement = document.getElementById('board');const colorSelect = document.getElementById('color-select');const startBtn = document.getElementById('start-btn');const restartBtn = document.getElementById('restart-btn');const statusElement = document.getElementById('status');const timerElement = document.getElementById('timer');// 初始化棋盘function initializeBoard() {boardElement.innerHTML = '';// 创建棋盘格子for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {const cell = document.createElement('div');cell.className = 'cell';// 标记星位if ((row === 3 || row === 7 || row === 11) && (col === 3 || col === 7 || col === 11)) {cell.classList.add('star');}cell.dataset.row = row;cell.dataset.col = col;cell.addEventListener('click', () => handleCellClick(row, col));boardElement.appendChild(cell);}}}// 开始游戏function startGame() {// 重置游戏状态gameState.board = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(EMPTY));gameState.gameOver = false;gameState.lastMove = null;gameState.humanColor = colorSelect.value === 'black' ? BLACK : WHITE;gameState.aiColor = gameState.humanColor === BLACK ? WHITE : BLACK;gameState.currentPlayer = BLACK; // 总是黑棋先手// 清除之前的胜利线和棋子if (gameState.winLine) {gameState.winLine.remove();gameState.winLine = null;}document.querySelectorAll('.stone').forEach(stone => stone.remove());// 更新UIstatusElement.textContent = gameState.currentPlayer === gameState.humanColor ? '轮到您下棋' : 'AI思考中...';statusElement.style.backgroundColor = '#f8f9fa';startBtn.disabled = true;restartBtn.disabled = false;// 如果是AI先手,则AI下棋if (gameState.currentPlayer === gameState.aiColor) {makeAIMove();}}// 处理格子点击function handleCellClick(row, col) {if (gameState.gameOver || gameState.thinking || gameState.currentPlayer !== gameState.humanColor) {return;}if (gameState.board[row][col] === EMPTY) {placeStone(row, col, gameState.humanColor);gameState.currentPlayer = gameState.aiColor;if (!checkWin(row, col, gameState.humanColor)) {statusElement.textContent = 'AI思考中...';setTimeout(() => makeAIMove(), 500); // 延迟500ms让玩家看到自己的棋子}}}// 放置棋子function placeStone(row, col, color) {gameState.board[row][col] = color;gameState.lastMove = { row, col };// 更新UIconst cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);const stone = document.createElement('div');stone.className = `stone ${color === BLACK ? 'black' : 'white'}`;// 移除上一个最后一步的标记document.querySelectorAll('.stone.last-move').forEach(s => s.classList.remove('last-move'));// 标记最后一步stone.classList.add('last-move');cell.appendChild(stone);}// AI走棋function makeAIMove() {if (gameState.gameOver) return;gameState.thinking = true;gameState.thinkingStart = Date.now();// 显示思考倒计时timerElement.textContent = 'AI思考中: 0.0s';timerElement.classList.add('thinking');const timerInterval = setInterval(() => {const elapsed = (Date.now() - gameState.thinkingStart) / 1000;timerElement.textContent = `AI思考中: ${elapsed.toFixed(1)}s`;}, 100);gameState.timerInterval = timerInterval;// 使用setTimeout确保UI更新setTimeout(() => {const startTime = Date.now();const { row, col } = findBestMove();const thinkTime = Date.now() - startTime;// 确保思考时间不超过最大限制if (thinkTime < MAX_THINKING_TIME) {setTimeout(() => {placeStone(row, col, gameState.aiColor);gameState.currentPlayer = gameState.humanColor;if (!checkWin(row, col, gameState.aiColor)) {statusElement.textContent = '轮到您下棋';}finishAITurn();}, Math.max(0, 500 - thinkTime)); // 确保至少有500ms的动画时间} else {// 如果超时,随机走一步const emptyCells = [];for (let r = 0; r < BOARD_SIZE; r++) {for (let c = 0; c < BOARD_SIZE; c++) {if (gameState.board[r][c] === EMPTY) {emptyCells.push({ row: r, col: c });}}}if (emptyCells.length > 0) {const randomMove = emptyCells[Math.floor(Math.random() * emptyCells.length)];placeStone(randomMove.row, randomMove.col, gameState.aiColor);gameState.currentPlayer = gameState.humanColor;statusElement.textContent = '轮到您下棋';}finishAITurn();}}, 100);}// 完成AI回合function finishAITurn() {gameState.thinking = false;clearInterval(gameState.timerInterval);timerElement.textContent = '';timerElement.classList.remove('thinking');}// 检查胜利条件function checkWin(row, col, color) {const directions = [[0, 1],   // 水平[1, 0],   // 垂直[1, 1],   // 对角线[1, -1]   // 反对角线];for (const [dx, dy] of directions) {let count = 1;// 正向检查for (let i = 1; i < 5; i++) {const r = row + i * dx;const c = col + i * dy;if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || gameState.board[r][c] !== color) {break;}count++;}// 反向检查for (let i = 1; i < 5; i++) {const r = row - i * dx;const c = col - i * dy;if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || gameState.board[r][c] !== color) {break;}count++;}if (count >= 5) {gameOver(color, row, col, dx, dy);return true;}}// 检查平局if (isBoardFull()) {gameOver(EMPTY);return true;}return false;}// 游戏结束处理function gameOver(winner, row, col, dx, dy) {gameState.gameOver = true;if (winner === EMPTY) {statusElement.textContent = '游戏结束:平局';statusElement.style.backgroundColor = '#fff3cd';} else {const isHumanWin = winner === gameState.humanColor;statusElement.textContent = isHumanWin ? '恭喜您获胜!' : 'AI获胜!';statusElement.style.backgroundColor = isHumanWin ? '#d4edda' : '#f8d7da';// 绘制胜利线if (row !== undefined && col !== undefined && dx !== undefined && dy !== undefined) {drawWinLine(row, col, dx, dy);}}}// 绘制胜利线function drawWinLine(row, col, dx, dy) {// 计算线的起点和终点let startRow = row;let startCol = col;let endRow = row;let endCol = col;// 找到线的起点for (let i = 1; i < 5; i++) {const r = row - i * dx;const c = col - i * dy;if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || gameState.board[r][c] !== gameState.board[row][col]) {break;}startRow = r;startCol = c;}// 找到线的终点for (let i = 1; i < 5; i++) {const r = row + i * dx;const c = col + i * dy;if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || gameState.board[r][c] !== gameState.board[row][col]) {break;}endRow = r;endCol = c;}// 创建线元素const line = document.createElement('div');line.className = 'win-line';// 计算线的位置和尺寸const boardRect = boardElement.getBoundingClientRect();const cellSize = boardRect.width / BOARD_SIZE;const startX = startCol * cellSize + cellSize / 2;const startY = startRow * cellSize + cellSize / 2;const endX = endCol * cellSize + cellSize / 2;const endY = endRow * cellSize + cellSize / 2;const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI;line.style.width = `${length}px`;line.style.height = '3px';line.style.left = `${startX}px`;line.style.top = `${startY}px`;line.style.transform = `rotate(${angle}deg)`;boardElement.parentNode.appendChild(line);gameState.winLine = line;}// 检查棋盘是否已满function isBoardFull() {for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (gameState.board[row][col] === EMPTY) {return false;}}}return true;}// 评估函数 - 评估当前棋盘对指定玩家的优势function evaluateBoard(board, player) {const opponent = player === BLACK ? WHITE : BLACK;let score = 0;// 检查所有可能的五子连线for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {// 水平方向if (col <= BOARD_SIZE - 5) {const line = [board[row][col],board[row][col + 1],board[row][col + 2],board[row][col + 3],board[row][col + 4]];score += evaluateLine(line, player, opponent);}// 垂直方向if (row <= BOARD_SIZE - 5) {const line = [board[row][col],board[row + 1][col],board[row + 2][col],board[row + 3][col],board[row + 4][col]];score += evaluateLine(line, player, opponent);}// 对角线方向if (row <= BOARD_SIZE - 5 && col <= BOARD_SIZE - 5) {const line = [board[row][col],board[row + 1][col + 1],board[row + 2][col + 2],board[row + 3][col + 3],board[row + 4][col + 4]];score += evaluateLine(line, player, opponent);}// 反对角线方向if (row <= BOARD_SIZE - 5 && col >= 4) {const line = [board[row][col],board[row + 1][col - 1],board[row + 2][col - 2],board[row + 3][col - 3],board[row + 4][col - 4]];score += evaluateLine(line, player, opponent);}}}return score;}// 评估一行五子的分数function evaluateLine(line, player, opponent) {let playerCount = 0;let opponentCount = 0;let emptyCount = 0;for (const cell of line) {if (cell === player) playerCount++;else if (cell === opponent) opponentCount++;else emptyCount++;}// 如果同时包含双方棋子,没有价值if (playerCount > 0 && opponentCount > 0) {return 0;}// 根据连子数量评分if (playerCount === 5) return 1000000; // 五连,胜利if (opponentCount === 5) return -1000000; // 对手五连,阻止if (playerCount === 4 && emptyCount === 1) return 10000; // 活四if (opponentCount === 4 && emptyCount === 1) return -10000; // 对手活四if (playerCount === 3 && emptyCount === 2) return 1000; // 活三if (opponentCount === 3 && emptyCount === 2) return -1000; // 对手活三if (playerCount === 2 && emptyCount === 3) return 100; // 活二if (opponentCount === 2 && emptyCount === 3) return -100; // 对手活二if (playerCount === 1 && emptyCount === 4) return 10; // 活一if (opponentCount === 1 && emptyCount === 4) return -10; // 对手活一return 0;}// 寻找最佳移动 - 使用极大极小算法与Alpha-Beta剪枝function findBestMove() {const startTime = Date.now();let bestScore = -Infinity;let bestMove = null;const depth = 3; // 搜索深度// 获取所有可能的移动const moves = getPossibleMoves(gameState.board);// 如果没有移动,返回nullif (moves.length === 0) {return { row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) };}// 遍历所有可能的移动for (const move of moves) {const { row, col } = move;// 尝试这个移动gameState.board[row][col] = gameState.aiColor;// 评估这个移动const score = minimax(gameState.board, depth - 1, -Infinity, Infinity, false, startTime);// 撤销移动gameState.board[row][col] = EMPTY;// 更新最佳移动if (score > bestScore || bestMove === null) {bestScore = score;bestMove = { row, col };}// 如果已经超时,立即返回当前最佳移动if (Date.now() - startTime > MAX_THINKING_TIME - 100) {break;}}return bestMove || moves[0]; // 如果没有找到最佳移动,返回第一个可能的移动}// 极大极小算法与Alpha-Beta剪枝function minimax(board, depth, alpha, beta, isMaximizing, startTime) {// 检查是否超时if (Date.now() - startTime > MAX_THINKING_TIME - 100) {return 0;}// 检查游戏是否结束或达到最大深度const winner = checkTerminal(board);if (winner !== null || depth === 0) {if (winner === gameState.aiColor) return 1000000;if (winner === gameState.humanColor) return -1000000;return evaluateBoard(board, gameState.aiColor);}// 获取所有可能的移动const moves = getPossibleMoves(board);if (isMaximizing) {let maxScore = -Infinity;for (const move of moves) {const { row, col } = move;board[row][col] = gameState.aiColor;const score = minimax(board, depth - 1, alpha, beta, false, startTime);board[row][col] = EMPTY;maxScore = Math.max(maxScore, score);alpha = Math.max(alpha, score);if (beta <= alpha) {break; // Beta剪枝}}return maxScore;} else {let minScore = Infinity;for (const move of moves) {const { row, col } = move;board[row][col] = gameState.humanColor;const score = minimax(board, depth - 1, alpha, beta, true, startTime);board[row][col] = EMPTY;minScore = Math.min(minScore, score);beta = Math.min(beta, score);if (beta <= alpha) {break; // Alpha剪枝}}return minScore;}}// 检查游戏是否结束function checkTerminal(board) {// 检查所有可能的五子连线for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] === EMPTY) continue;const color = board[row][col];// 水平方向if (col <= BOARD_SIZE - 5) {let win = true;for (let i = 1; i < 5; i++) {if (board[row][col + i] !== color) {win = false;break;}}if (win) return color;}// 垂直方向if (row <= BOARD_SIZE - 5) {let win = true;for (let i = 1; i < 5; i++) {if (board[row + i][col] !== color) {win = false;break;}}if (win) return color;}// 对角线方向if (row <= BOARD_SIZE - 5 && col <= BOARD_SIZE - 5) {let win = true;for (let i = 1; i < 5; i++) {if (board[row + i][col + i] !== color) {win = false;break;}}if (win) return color;}// 反对角线方向if (row <= BOARD_SIZE - 5 && col >= 4) {let win = true;for (let i = 1; i < 5; i++) {if (board[row + i][col - i] !== color) {win = false;break;}}if (win) return color;}}}// 检查平局let isFull = true;for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] === EMPTY) {isFull = false;break;}}if (!isFull) break;}return isFull ? EMPTY : null;}// 获取所有可能的移动(优化:只考虑已有棋子周围的空位)function getPossibleMoves(board) {const moves = [];const directions = [[-1, -1], [-1, 0], [-1, 1],[0, -1],          [0, 1],[1, -1],  [1, 0], [1, 1]];// 先检查棋盘上是否有棋子let hasStone = false;for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] !== EMPTY) {hasStone = true;break;}}if (hasStone) break;}// 如果棋盘为空,返回中心点if (!hasStone) {return [{ row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) }];}// 否则,收集所有已有棋子周围的空位const considered = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(false));for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] !== EMPTY) {// 检查周围的8个方向for (const [dx, dy] of directions) {const newRow = row + dx;const newCol = col + dy;if (newRow >= 0 && newRow < BOARD_SIZE && newCol >= 0 && newCol < BOARD_SIZE && board[newRow][newCol] === EMPTY && !considered[newRow][newCol]) {moves.push({ row: newRow, col: newCol });considered[newRow][newCol] = true;}}}}}// 如果没有找到可能的移动(理论上不应该发生),返回所有空位if (moves.length === 0) {for (let row = 0; row < BOARD_SIZE; row++) {for (let col = 0; col < BOARD_SIZE; col++) {if (board[row][col] === EMPTY) {moves.push({ row, col });}}}}return moves;}// 事件监听startBtn.addEventListener('click', startGame);restartBtn.addEventListener('click', startGame);// 初始化initializeBoard();</script>
</body>
</html>

🏆 六、总结:从玩具到工程的蜕变

通过这个项目,我们实现了:

  1. 完整的游戏循环架构
  2. 经典的博弈树搜索算法
  3. 专业级的性能优化技巧
  4. 优雅的前端交互设计

学习价值

  • 前端开发者 → 掌握复杂状态管理
  • 算法爱好者 → 理解博弈树搜索
  • 游戏开发者 → 学习AI设计模式

“围棋是上帝的游戏,五子棋则是人类智慧的结晶。” —— 通过这个项目,我们正在用代码重现这种智慧。

相关文章:

【硬核实战】从零打造智能五子棋AI:JavaScript实现与算法深度解析

&#x1f31f;【硬核实战】从零打造智能五子棋AI&#xff1a;JavaScript实现与算法深度解析&#x1f31f; &#x1f4dc; 前言&#xff1a;当传统棋艺遇上人工智能 五子棋作为中国传统棋类游戏&#xff0c;规则简单却变化无穷。本文将带你用纯前端技术实现一个具备AI对战功能…...

使用 kind 创建 K8s 集群并部署 StarRocks 的完整指南

使用 kind 创建 K8s 集群并部署 StarRocks 的完整指南 本文档详细介绍如何使用 kind 创建 Kubernetes 集群&#xff0c;并在其上使用 Helm 部署 StarRocks 集群&#xff08;非高可用模式&#xff09;。同时也包括如何访问 StarRocks 集群并导入数据。 目录 前提条件参考文档…...

华为OD全流程解析+备考攻略+经验分享

华为OD全流程解析&#xff0c;备考攻略 快捷目录 华为OD全流程解析&#xff0c;备考攻略一、什么是华为OD&#xff1f;二、什么是华为OD机试&#xff1f;三、华为OD面试流程四、华为OD薪资待遇及职级体系五、ABCDE卷类型及特点六、题型与考点七、机试备考策略八、薪资与转正九、…...

数据库中的数组: MySQL与StarRocks的数组操作解析

在现代数据处理中, 数组 (Array) 作为一种高效存储和操作结构化数据的方式, 被广泛应用于日志分析, 用户行为统计, 标签系统等场景. 然而, 不同数据库对数组的支持差异显著. 本文将以MySQL和StarRocks为例, 深入解析它们的数组操作能力, 并对比其适用场景. 文章目录 一 为什么需…...

Qt 交叉编译详细配置指南

一、Qt 交叉编译详细配置 1. 准备工作 1.1 安装交叉编译工具链 # 例如安装ARM工具链(Ubuntu/Debian) sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf# 或者64位ARM sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu 1.2 准备目标…...

【详细图文】在VScode中配置python开发环境

目录 一、下载安装VSCode 1、官网下载VSCode 2、安装VSCode 3、汉化vscode &#xff08;1&#xff09;已自动下载汉化版插件 &#xff08;2&#xff09;未自动下载汉化版插件 二、 下载安装Python 1、官网下载Python 2、安装Python &#xff08;1&#xff09;双击打开…...

strings.Fields 使用详解

目录 1. 官方包 2. 支持版本 3. 官方说明 4. 作用 5. 实现原理 6. 推荐使用场景和不推荐使用场景 推荐场景 不推荐场景 7. 使用场景示例 示例1&#xff1a;官方示例 示例2&#xff1a;解析服务器日志 示例3&#xff1a;清理用户输入 8. 性能比较 性能特点 对比表…...

PCI认证 密钥注入 ECC算法工具 NID_secp521r1 国密算法 openssl 全套证书生成,从证书提取公私钥数组 x,y等

步骤 1.全套证书已经生成。OK 2.找国芯要ECC加密解密签名验签代码。给的逻辑说明没有示例代码很难的上。 3.集成到工具 与SP联调。 1.用openssl全套证书生成及验证 注意&#xff1a;这里CA 签发 KLD 证书用的是SHA256。因为芯片只支持SHA256算法,不支持SHA512。改成统一。…...

微软 SC-900 认证-考核Azure 和 Microsoft 365中的安全、合规和身份管理(SCI)概念

微软 SC-900 认证介绍 SC-900 认证考试是微软推出的一项基础级别认证&#xff0c;全称为 Microsoft Certified: Security, Compliance, and Identity Fundamentals。该认证旨在验证考生对微软云服务&#xff08;如 Azure 和 Microsoft 365&#xff09;中的安全、合规和身份管理…...

Uniapp 集成极光推送(JPush)完整指南

文章目录 前言一、准备工作1. 注册极光开发者账号2. 创建应用3. Uniapp项目准备 二、集成极光推送插件方法一&#xff1a;使用UniPush&#xff08;推荐&#xff09;方法二&#xff1a;手动集成极光推送SDK 三、配置原生平台参数四、核心功能实现1. 获取RegistrationID2. 设置别…...

OpenCV图像平滑处理方法详解

文章目录 引言一、什么是图像平滑&#xff1f;二、常见的图像平滑方法1.先对图片加上噪声点2. 均值滤波&#xff08;Averaging&#xff09;3. 方框滤波&#xff08;boxFilter&#xff09;4. 中值滤波&#xff08;Median Blur&#xff09;5. 高斯滤波&#xff08;Gaussian Blur&…...

Lua 中,`math.random` 的详细用法

在 Lua 中&#xff0c;math.random 是用于生成伪随机数的核心函数。以下是其详细用法、注意事项及常见问题的解决方案&#xff1a; Lua 中&#xff0c;math.random 的详细用法—目录 一、基础用法1. 生成随机浮点数&#xff08;0 ≤ x < 1&#xff09;2. 生成指定范围的随机…...

使用PX4,gazebo,mavros为旋翼添加下视的相机(仿真采集openrealm数据集-第一步)

目录 一.方法一&#xff08;没成功&#xff09; 1.运行PX4 2.运行mavros通讯 3.启动仿真世界和无人机 &#xff08;1&#xff09;单独测试相机 &#xff08;2&#xff09;make px4_sitl gazebo启动四旋翼iris无人机 二.方法二&#xff08;成功&#xff09; 1.通过 rosl…...

ATEngin开发记录_4_使用Premake5 自动化构建跨平台项目文件

该系列只做记录 不做教程 所以文章简洁直接 会列出碰到的问题和解决方案 只适合C萌新 文章目录 Permake5为什么使用 Premake&#xff1f; 项目实战总结一下&#xff1a;详细代码: Permake5 Premake5 是一个跨平台的构建配置工具&#xff0c;它允许开发者通过使用一个简单的脚…...

equals() 和 hashCode()

作为 Java 开发者&#xff0c;我们经常会用到 equals() 和 hashCode() 这两个方法。 它们是 Object 类中定义的基础方法&#xff0c;看似简单&#xff0c;但如果理解不透彻&#xff0c;很容易在实际开发中踩坑。 本文将深入探讨这两个方法的作用、区别、以及如何正确地重写它们…...

臭氧除菌柜市场报告:2031年全球臭氧除菌柜市场销售额预计将达到9.4亿元

一、市场概述 &#xff08;一&#xff09;定义与分类 臭氧除菌柜&#xff0c;作为新一代绿色消毒设备&#xff0c;主要利用臭氧&#xff08;O₃&#xff09;的强氧化性来实现无化学残留的消毒净化。根据产品类型&#xff0c;可分为单门型和双门型。单门型设计紧凑&#xff0c…...

解决python manage.py shell ModuleNotFoundError: No module named xxx

报错如下&#xff1a; python manage.py shellTraceback (most recent call last):File "/Users/z/Documents/project/c/manage.py", line 10, in <module>execute_from_command_line(sys.argv)File "/Users/z/.virtualenvs/c/lib/python3.12/site-packa…...

通用接口函数注册模块设计与实现

文章目录 通用接口函数注册模块设计与实现1. 模块概述2. 核心功能2.1 数据结构函数注册项结构体注册函数宏 2.2 核心函数实现函数&#xff1a;sl_register_interface_functions 3. 使用示例3.1 基础使用示例 - 设备驱动接口定义接口结构体实现具体函数创建注册表注册接口 3.2 高…...

C,C++,C#

C、C 和 C# 是三种不同的编程语言&#xff0c;虽然它们名称相似&#xff0c;但在设计目标、语法特性、运行环境和应用场景上有显著区别。以下是它们的核心区别&#xff1a; 1. 设计目标和历史 语言诞生时间设计目标特点C1972&#xff08;贝尔实验室&#xff09;面向过程&#…...

scala-集合3

集合计算高级函数 过滤&#xff1a;遍历一个集合并从中获取满足指定条件的元素组成一个新的集合 &#xff08;筛选出满足条件的元素组成新集合。&#xff09; 转换或映射&#xff08;map&#xff09;&#xff1a;将原始集合中的元素映射到某个函数。 扁平化&#xff1a;取消…...

Spring MVC 重定向(Redirect)详解

Spring MVC 重定向&#xff08;Redirect&#xff09;详解 1. 核心概念与作用 重定向&#xff08;Redirect&#xff09; 是 Spring MVC 中一种客户端重定向机制&#xff0c;通过 HTTP 302 状态码&#xff08;默认&#xff09;将用户浏览器重定向到指定 URL。 主要用途&#xf…...

Scala的集合(二)

1. 集合计算高集函数 任务要求 1)过滤:遍历一个集合并从中获取满足指定条件的元素组成一个新的集合 2)转化/映射&#xff08;map&#xff09;&#xff1a;将集合中的每一个元素映射到某一个函数 3)扁平化 4)扁平化映射 注&#xff1a;flatMap 相当于先进行 map 操作&#…...

GZ036区块链卷三 EtherGame合约漏洞详解

题目 pragma solidity ^0.8.3; contract EtherGame {uint public targetAmount 7 ether;address public winner;function deposit() public payable {require(msg.value 1 ether, "You can only send 1 Ether");uint balance address(this).balance;require(bala…...

BGP路由协议之路由通告/传递

BGP 的路由宣告 BGP 自身并不会发现并计算产生路由&#xff0c;只会将 IGP 路由表中的路由引入到 BGP 路由表中&#xff0c;并通过 Update 报文传递给 BGP 对等体&#xff08;邻居&#xff09; ​Network​ 宣告&#xff0c;前提是路由表中存在该条路由 ​import-route​ 引…...

Python合并多个pdf

场景&#xff1a; 现在要解决批量合并PDF的问题。 有很多PDF文件需要合并成一个&#xff0c;比如报告、发票或者多个章节的文档。 对于Windows用户&#xff0c;Adobe Acrobat是专业的选择&#xff0c;但需要付费。但是我不想花钱&#xff0c;所以推荐免费软件&#xff0c;比…...

聊一聊接口测试时遇到上下游依赖时该如何测试

目录 一、手工测试时的处理方法 1.1沟通协调法 1.2模拟数据法 二、自动化测试时的处理方法 2.1 数据关联法&#xff08;变量提取&#xff09; 2.2 Mock数据法 2.3自动化框架中的依赖管理 三、实施示例&#xff08;以订单接口测试为例&#xff09; 3.1Mock依赖接口&…...

pdf转latex

Doc2X&#xff08;https://doc2x.noedgeai.com/&#xff09; Doc2X 是一个由 NoEdgeAI 提供的在线工具&#xff0c;主要用于将 PDF 文件&#xff08;尤其是学术论文、报告等文档&#xff09;转换为 LaTeX 格式。LaTeX 是一种高质量排版系统&#xff0c;广泛应用于学术界和出版…...

剖析 Docker Swarm 操作对原有容器端口影响

剖析 Docker Swarm 操作对容器端口影响 一、背景阐述 在使用 Docker Swarm 构建集群环境过程中&#xff0c;于 ts3 节点出现了原有的容器端口全部失效&#xff0c;手动重启后才恢复的情况。期间涉及 docker swarm init --advertise-addr172.16.10.110 以及 docker swarm join…...

QML面试笔记--UI设计篇02布局控件

1. QML 中常用的布局控件 1.1. Row1.2. Column1.3. Grid1.4. RowLayout1.5. ColumnLayout1.6. GridLayout1.7. 总结 1. QML 中常用的布局控件 1.1. Row 背景知识&#xff1a;Row 布局用于将子元素水平排列&#xff0c;适合简单的线性布局&#xff0c;如工具栏按钮或表单输入…...

Java全栈项目--校园快递管理与配送系统(4)

源代码续 /*** 通知工具类*/// 通知类型常量 export const NotificationType {SYSTEM: 1,EXPRESS: 2,ACTIVITY: 3 }// 通知类型名称映射 export const NotificationTypeNames {[NotificationType.SYSTEM]: 系统通知,[NotificationType.EXPRESS]: 快递通知,[NotificationType…...

c语言练习一

1、统计二进制数中1的个数 #include <stdio.h>int main(void) {int count 0; //统计1出现次数 int x 0b1011;while(x){count ;//x 0b1011 > x-1 0b1010 x-1,将x从右往左数遇到第一个1变成0&#xff0c;左边全部变为1&#xff0c;右边不变 //x&x-1 1010 …...

Scala安装

Spark安装 Spark的Local模式仅需要单个虚拟机节点即可&#xff0c;无需启动hadoop集群。实验步骤如下&#xff1a; 将spark的安装包上传到虚拟机node01中&#xff08;建议路径&#xff1a;/opt/software/spark&#xff09;并解压缩文件。将解压文件夹重命名为spark-local 解…...

爱普生RTC模块RA8804CE在ADAS域控制器的应用

在汽车智能化、自动化飞速发展的时代&#xff0c;ADAS&#xff08;高级驾驶辅助系统&#xff09;的多传感器融合与实时决策高度依赖精准的时间基准。毫秒级的时间偏差可能导致传感器数据错位&#xff0c;直接影响行车安全。爱普生RA8804CE实时时钟模块凭借其内置的32.768 kHz晶…...

开箱即用!推荐一款Python开源项目:DashGo,支持定制改造为测试平台!

大家好&#xff0c;我是狂师。 市面上的开源后台管理系统项目层出不穷&#xff0c;对应所使用到的技术栈也不尽相同。 今天给大家推荐一款开源后台管理系统: DashGo&#xff0c;不仅部署起来非常的简单&#xff0c;而且它是基于Python技术栈实现的&#xff0c;使得基于它进行…...

C++使用WebView2控件,通过IPC通信与Javascript交互

引言 在现代桌面应用程序开发中&#xff0c;Web技术与原生应用的融合变得越来越普遍。Microsoft的WebView2控件为C开发者提供了一个强大的工具&#xff0c;使他们能够在桌面应用中嵌入基于Chromium的Web浏览器引擎。本文将详细介绍如何在C应用程序中使用WebView2控件&#xff…...

算法中Hash备胎——LRU的设计与实现

核心内容1.理解LRU的原理2.理解LRU是如何实现的3.能够通过代码实现LRU 缓存是应用软件的必备功能之一&#xff0c;在操作系统、Java里的Spring、mybatis、redis、mysql等软件中都有自己的内部缓存模块&#xff0c;而缓存是如何实现的&#xff1f; 在操作系统教科书里我们知道…...

Profinet邂逅ModbusRTU:印刷厂有网关“一键打通”通信链路

Profinet邂逅ModbusRTU&#xff1a;印刷厂有网关“一键打通”通信链路 在现代化印刷厂的生产线上&#xff0c;高效稳定的设备通信是保障印刷质量和生产效率的关键。某印刷厂的印刷机控制系统采用了西门子PLC进行自动化控制&#xff0c;同时还有众多基于ModbusRTU协议的传感器和…...

Spring中使用Kafka的详细配置,以及如何集成 KRaft 模式的 Kafka

在 Spring 中使用 Apache Kafka 的配置主要涉及 Spring Boot Starter for Kafka&#xff0c;而开启 ‌KRaft 模式‌&#xff08;Kafka 的元数据管理新模式&#xff0c;替代 ZooKeeper&#xff09;需要特定的 Kafka Broker 配置。以下是详细步骤&#xff1a; 一、Spring 中集成 …...

C++之继承

本节我们将要学习C作为面向对象语言的三大特性之一的继承。 前言 一、继承的概念 二、继承的定义 2.1 定义格式 2.2 继承基类成员访问方式的变化 2.3 继承类模板 三、基类和派生类之间的转换 四、继承中的作用域 五、派生类的默认成员函数 六、实现一个不能被继承的类 七、继承…...

服务器申请 SSL 证书注意事项

一、确认证书类型 基础域名型&#xff08;DV&#xff09;&#xff1a;这类证书验证速度最快&#xff0c;通常只需验证域名所有权&#xff0c;几分钟到几小时即可颁发。适用于个人博客、小型企业展示网站等对安全性要求不是顶级严苛&#xff0c;且急需启用 HTTPS 的场景。但它仅…...

【资料分享】全志T536(异构多核ARMCortex-A55+玄铁E907 RISC-V)工业核心板说明书

核心板简介 创龙科技SOM-TLT536是一款基于全志科技T536MX-CEN2/T536MX-CXX四核ARM Cortex-A55 +...

博途 TIA Portal之1200做从站与调试助手的TCP通讯

其实&#xff0c;1200做从站与调试助手做TCP通讯很简单&#xff0c;只需要在组态时把“主动建立连接”放在对侧即可。但是我们还是要从头讲起&#xff0c;以方便没有基础的朋友能直接上手操作。 1、硬件准备 1200PLC一台&#xff0c;带调试助手的PC机一台&#xff0c;调试助手…...

移动端六大语言速记:第9部分 - 并发与多线程

移动端六大语言速记:第9部分 - 并发与多线程 本文将对比Java、Kotlin、Flutter(Dart)、Python、ArkTS和Swift这六种移动端开发语言在并发与多线程方面的特性,帮助开发者理解和掌握各语言的并发编程机制。 9. 并发与多线程 9.1 线程与进程 各语言线程与进程的创建和管理方…...

基于大模型的ALS预测与手术优化系统技术方案

目录 技术方案文档:基于大模型的ALS预测与手术优化系统1. 数据预处理与特征工程模块流程图伪代码2. 多模态融合预测模型模型架构图伪代码3. 术中实时监测与动态干预系统系统流程图伪代码4. 统计验证与可解释性模块验证流程图伪代码示例(SHAP分析)5. 健康教育与交互系统系统架…...

【Vue3知识】组件间通信的方式

组件间通信的方式 概述**1. 父子组件通信****父组件向子组件传递数据&#xff08;Props&#xff09;****子组件向父组件发送事件&#xff08;自定义事件&#xff09;** **2. 兄弟组件通信****通过父组件中转****使用全局状态管理&#xff08;如 Pinia 或 Vuex&#xff09;** **…...

【数据挖掘】岭回归(Ridge Regression)和线性回归(Linear Regression)对比实验

这是一个非常实用的 岭回归&#xff08;Ridge Regression&#xff09;和线性回归&#xff08;Linear Regression&#xff09;对比实验&#xff0c;使用了 scikit-learn 中的 California Housing 数据集 来预测房价。 &#x1f4e6; 第一步&#xff1a;导入必要的库 import num…...

RuntimeError: Error(s) in loading state_dict for ChartParser

一 bug错误 最近使用千问大模型有一个bug&#xff0c;报错信息如下 raise RuntimeError(Error(s) in loading state_dict for {}:\n\t{}.format( RuntimeError: Error(s) in loading state_dict for ChartParser:Unexpected key(s) in state_dict: "pretrained_model.em…...

汽车无钥匙启动125KHz低频发射天线工作原理

汽车智能钥匙低频天线是无钥匙进入&#xff08;PE&#xff09;及无钥匙启动&#xff08;PS&#xff09;系统的一部分&#xff0c;主要负责发送低频信号&#xff0c;探测智能钥匙与各低频天线间的相对位置&#xff0c;判断车内是否存在智能钥匙。 支持PEPS系统实现便捷操作 无…...

【Docker基础-镜像】--查阅笔记2

目录 Docker镜像概述base镜像镜像的分层结构镜像的理解镜像的构建docker commit 制作镜像DockerfileDockerfile 指令FROMLABELRUNARGENVADDCOPYWORKDIRUSERVOLUMEEXPOSECMD 和 ENTRYPOINT Docker镜像概述 镜像是Docker容器的基石&#xff0c;容器是镜像的运行实例&#xff0c;…...

LeetCode 第47题:旋转数组

LeetCode 第47题&#xff1a;旋转数组 题目描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例1&#xff1a; 输入&#xf…...