用 HTML、CSS 和 JavaScript 實現五子棋人機對戰游戲

引言

在 Web 開發的世界里,通過 HTML、CSS 和 JavaScript 可以創造出各種各樣有趣的互動游戲。今天,我們將深入探討如何實現一個簡單而又富有挑戰性的五子棋人機對戰游戲。這個游戲不僅能讓你重溫經典的五子棋玩法,還能通過 AI 對戰功能給你帶來全新的挑戰。

項目概述

五子棋游戲將采用 HTML 構建頁面結構,CSS 進行樣式設計,JavaScript 實現游戲的邏輯和交互。整個游戲界面將包含一個棋盤、游戲狀態信息、控制按鈕以及勝利提示模態框等元素。

效果圖?

界面設計

開發中使用了 Tailwind CSS 框架來快速搭建界面。以下是界面的主要組成部分:

  1. 游戲標題和描述:在頁面頂部顯示游戲的標題和簡單描述,讓玩家快速了解游戲內容。
  2. 游戲區域:使用?canvas?元素繪制棋盤,玩家可以通過點擊棋盤上的交叉點來落子。
  3. 游戲信息面板:顯示當前回合、游戲時間、步數和游戲模式等信息,方便玩家了解游戲狀態。
  4. 游戲規則說明:列出五子棋的基本規則,幫助新手玩家快速上手。
  5. 控制按鈕:提供重新開始、悔棋、切換游戲模式等功能,增強游戲的交互性。
  6. 勝利提示模態框:當游戲結束時,顯示獲勝方信息,并提供開始新游戲的按鈕。

游戲邏輯實現

JavaScript 部分是游戲的核心,負責處理游戲的各種邏輯,包括棋盤繪制、落子處理、勝利判斷和 AI 對戰等功能。

棋盤繪制

使用?canvas?的繪圖 API 繪制棋盤的網格線、天元和星位,并根據游戲狀態繪制棋子。以下是繪制棋盤的代碼片段:

// 繪制棋盤
function drawBoard() {ctx.clearRect(0, 0, canvas.width, canvas.height);// 繪制網格線ctx.strokeStyle = '#8B4513';ctx.lineWidth = 1.5;for (let i = 0; i < BOARD_SIZE; i++) {// 水平線ctx.beginPath();ctx.moveTo(0, i * CELL_SIZE);ctx.lineTo(canvas.width, i * CELL_SIZE);ctx.stroke();// 垂直線ctx.beginPath();ctx.moveTo(i * CELL_SIZE, 0);ctx.lineTo(i * CELL_SIZE, canvas.height);ctx.stroke();}// 繪制天元和星位const starPoints = [{x: 3, y: 3}, {x: 3, y: 11}, {x: 7, y: 7}, {x: 11, y: 3}, {x: 11, y: 11}];starPoints.forEach(point => {ctx.beginPath();ctx.arc(point.x * CELL_SIZE, point.y * CELL_SIZE, 4, 0, Math.PI * 2);ctx.fillStyle = '#8B4513';ctx.fill();});// 繪制棋子for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] !== 0) {drawPiece(i, j, gameBoard[i][j]);}}}
}
落子處理

當玩家點擊棋盤時,程序會計算點擊位置對應的格子坐標,并檢查該位置是否合法。如果合法,則在該位置落子,并更新游戲狀態。以下是處理點擊事件的代碼片段:

// 點擊棋盤事件
function handleCanvasClick(e) {if (!gameActive) return;// 如果是AI模式且當前是AI回合,不允許玩家落子if (isAiMode && currentPlayer !== playerPiece) {return;}const rect = canvas.getBoundingClientRect();const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;// 計算點擊的格子坐標const x = (e.clientX - rect.left) * scaleX;const y = (e.clientY - rect.top) * scaleY;const col = Math.round(x / CELL_SIZE);const row = Math.round(y / CELL_SIZE);// 檢查坐標是否在棋盤內且為空if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && gameBoard[row][col] === 0) {// 落子gameBoard[row][col] = currentPlayer;moveHistory.push({ row, col, player: currentPlayer });// 添加落子動畫效果drawBoard();// 檢查是否勝利if (checkWin(row, col, currentPlayer)) {showWinModal(currentPlayer);return;}// 檢查是否平局if (checkDraw()) {gameActive = false;stopTimer();statusText.textContent = '游戲結束 - 平局!';return;}// 切換玩家currentPlayer = currentPlayer === 1 ? 2 : 1;updateGameStatus();// 如果是AI模式且現在是AI回合,讓AI落子if (isAiMode && currentPlayer !== playerPiece) {setTimeout(makeAiMove, 500);}}
}
勝利判斷

在每次落子后,程序會檢查是否有一方在橫、豎或斜方向形成五子連線。如果有,則判定該方獲勝。以下是檢查勝利條件的代碼片段:

// 檢查勝利條件
function checkWin(row, col, player) {const directions = [[1, 0],   // 水平[0, 1],   // 垂直[1, 1],   // 對角線[1, -1]   // 反對角線];for (const [dx, dy] of directions) {let count = 1;  // 當前位置已經有一個棋子// 正向檢查for (let i = 1; i < 5; i++) {const newRow = row + i * dy;const newCol = col + i * dx;if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {break;}if (gameBoard[newRow][newCol] === player) {count++;} else {break;}}// 反向檢查for (let i = 1; i < 5; i++) {const newRow = row - i * dy;const newCol = col - i * dx;if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {break;}if (gameBoard[newRow][newCol] === player) {count++;} else {break;}}if (count >= 5) {return true;}}return false;
}

AI 對戰

AI 對戰是游戲的亮點之一。我們使用了一個簡單的評估函數來評估棋盤狀態,并通過遍歷所有可能的落子位置,選擇得分最高的位置作為最佳落子位置。以下是 AI 相關的代碼片段:

// AI落子
function makeAiMove() {if (!gameActive || !isAiMode || currentPlayer === playerPiece) {return;}showAiThinking();// 模擬AI思考時間setTimeout(() => {const aiPiece = playerPiece === 1 ? 2 : 1;const bestMove = findBestMove(aiPiece);if (bestMove) {const { row, col } = bestMove;// 落子gameBoard[row][col] = aiPiece;moveHistory.push({ row, col, player: aiPiece });// 添加落子動畫效果drawBoard();// 檢查是否勝利if (checkWin(row, col, aiPiece)) {hideAiThinking();showWinModal(aiPiece);return;}// 檢查是否平局if (checkDraw()) {gameActive = false;stopTimer();statusText.textContent = '游戲結束 - 平局!';hideAiThinking();return;}// 切換玩家currentPlayer = playerPiece;updateGameStatus();hideAiThinking();}}, 800);
}// AI評估函數 - 評估棋盤狀態的分數
function evaluateBoard(player) {const opponent = player === 1 ? 2 : 1;let score = 0;// 檢查所有可能的5連位置for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] === 0) {continue;}// 四個方向檢查const directions = [[1, 0],   // 水平[0, 1],   // 垂直[1, 1],   // 對角線[1, -1]   // 反對角線];for (const [dx, dy] of directions) {// 檢查是否會超出邊界if (i + 4 * dy >= BOARD_SIZE || i + 4 * dy < 0 || j + 4 * dx >= BOARD_SIZE || j + 4 * dx < 0) {continue;}// 計算當前方向的連子數let count = 1;let emptySpaces = 0;let blockedEnds = 0;// 正向檢查for (let k = 1; k < 5; k++) {const row = i + k * dy;const col = j + k * dx;if (gameBoard[row][col] === gameBoard[i][j]) {count++;} else if (gameBoard[row][col] === 0) {emptySpaces++;break;} else {blockedEnds++;break;}}// 反向檢查for (let k = 1; k < 5; k++) {const row = i - k * dy;const col = j - k * dx;if (gameBoard[row][col] === gameBoard[i][j]) {count++;} else if (gameBoard[row][col] === 0) {emptySpaces++;break;} else {blockedEnds++;break;}}// 如果兩端都被堵,這條線無效if (blockedEnds === 2) {continue;}// 根據連子數和開放端計算分數const isPlayerPiece = gameBoard[i][j] === player;let lineScore = 0;if (count >= 5) {lineScore = isPlayerPiece ? 10000 : -10000; // 勝利} else if (count === 4) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 1000 : -1000; // 活四} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 100 : -100; // 沖四}} else if (count === 3) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 100 : -100; // 活三} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 10 : -10; // 沖三}} else if (count === 2) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 5 : -5; // 活二} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 1 : -1; // 沖二}}score += lineScore;}}}return score;
}// AI尋找最佳落子位置
function findBestMove(player) {const opponent = player === 1 ? 2 : 1;let bestScore = -Infinity;let bestMove = null;// 只考慮已有棋子周圍的空位const possibleMoves = getPossibleMoves();for (const { row, col } of possibleMoves) {// 嘗試落子gameBoard[row][col] = player;// 檢查是否直接獲勝if (checkWin(row, col, player)) {gameBoard[row][col] = 0; // 恢復棋盤return { row, col };}// 評估這個位置的分數let score = evaluateBoard(player);// 恢復棋盤gameBoard[row][col] = 0;// 更新最佳移動if (score > bestScore) {bestScore = score;bestMove = { row, col };}}// 如果沒有找到最佳移動,隨機選擇一個空位if (!bestMove && possibleMoves.length > 0) {const randomIndex = Math.floor(Math.random() * possibleMoves.length);return possibleMoves[randomIndex];}// 如果棋盤為空,選擇中心點if (!bestMove) {return { row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) };}return bestMove;
}

完整代碼

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>五子棋游戲</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet"><script>tailwind.config = {theme: {extend: {colors: {primary: '#8B5A2B',secondary: '#D2B48C',board: '#DEB887',black: '#000000',white: '#FFFFFF',ai: '#FF6B6B',},fontFamily: {sans: ['Inter', 'system-ui', 'sans-serif'],},}}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.board-grid {background-size: 100% 100%;background-image: linear-gradient(to right, rgba(0,0,0,0.6) 1px, transparent 1px),linear-gradient(to bottom, rgba(0,0,0,0.6) 1px, transparent 1px);}.piece-shadow {filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));}.piece-transition {transition: all 0.2s ease-out;}.btn-hover {transition: all 0.2s ease;}.btn-hover:hover {transform: translateY(-2px);box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);}.pulse-animation {animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;}@keyframes pulse {0%, 100% {opacity: 1;}50% {opacity: 0.5;}}}</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4 font-sans"><div class="max-w-4xl w-full bg-white rounded-2xl shadow-xl overflow-hidden"><div class="bg-primary text-white p-6 text-center"><h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">五子棋</h1><p class="text-secondary mt-2">經典對弈游戲</p></div><div class="p-6 md:p-8 flex flex-col md:flex-row gap-6"><!-- 游戲區域 --><div class="flex-1 relative"><div class="aspect-square bg-board rounded-lg shadow-lg overflow-hidden board-grid" style="background-size: calc(100% / 14) calc(100% / 14);"><canvas id="gameCanvas" class="w-full h-full cursor-pointer"></canvas></div><div id="gameStatus" class="mt-4 p-3 bg-secondary/20 rounded-lg text-center"><p id="statusText" class="font-medium">游戲開始! 黑棋先行</p></div></div><!-- 游戲控制和信息 --><div class="w-full md:w-80 flex flex-col gap-6"><div class="bg-gray-50 rounded-lg p-5 shadow-sm"><h2 class="text-lg font-semibold mb-3 flex items-center"><i class="fa-solid fa-info-circle mr-2 text-primary"></i>游戲信息</h2><div class="space-y-3"><div class="flex items-center justify-between"><span class="text-gray-600">當前回合</span><div class="flex items-center"><div id="currentPlayer" class="w-6 h-6 rounded-full bg-black mr-2 piece-shadow"></div><span id="playerText">黑棋</span></div></div><div class="flex items-center justify-between"><span class="text-gray-600">游戲時間</span><span id="gameTime" class="font-mono">00:00</span></div><div class="flex items-center justify-between"><span class="text-gray-600">步數</span><span id="moveCount">0</span></div><div class="flex items-center justify-between"><span class="text-gray-600">游戲模式</span><div id="gameMode" class="flex items-center"><div class="w-3 h-3 rounded-full bg-green-500 mr-2"></div><span id="modeText">人機對戰</span></div></div></div></div><div class="bg-gray-50 rounded-lg p-5 shadow-sm"><h2 class="text-lg font-semibold mb-3 flex items-center"><i class="fa-solid fa-crown mr-2 text-primary"></i>游戲規則</h2><ul class="text-sm text-gray-600 space-y-2"><li class="flex items-start"><i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i><span>黑棋和白棋輪流在棋盤上落子</span></li><li class="flex items-start"><i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i><span>先在橫、豎或斜方向形成五子連線者獲勝</span></li><li class="flex items-start"><i class="fa-solid fa-circle text-xs mt-1.5 mr-2 text-primary"></i><span>點擊棋盤上的交叉點放置棋子</span></li></ul></div><div class="flex flex-col gap-3"><div class="flex gap-3"><button id="restartBtn" class="flex-1 bg-primary hover:bg-primary/90 text-white py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center"><i class="fa-solid fa-refresh mr-2"></i>重新開始</button><button id="undoBtn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center"><i class="fa-solid fa-undo mr-2"></i>悔棋</button></div><div class="flex gap-3"><button id="humanBtn" class="flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center"><i class="fa-solid fa-user mr-2"></i>人人對戰</button><button id="aiBtn" class="flex-1 bg-green-500 hover:bg-green-600 text-white py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center"><i class="fa-solid fa-robot mr-2"></i>人機對戰</button></div></div></div></div><div class="bg-gray-50 p-4 text-center text-sm text-gray-500"><p>? 2025 五子棋游戲 | 一個簡單的 Web 游戲</p></div></div><!-- 勝利提示模態框 --><div id="winModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden opacity-0 transition-opacity duration-300"><div class="bg-white rounded-xl p-8 max-w-md w-full mx-4 transform transition-transform duration-300 scale-95"><div class="text-center"><div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"><i class="fa-solid fa-trophy text-3xl text-yellow-500"></i></div><h2 class="text-2xl font-bold mb-2" id="winnerText">黑棋獲勝!</h2><p class="text-gray-600 mb-6">恭喜您贏得了這場精彩的比賽!</p><button id="newGameBtn" class="bg-primary hover:bg-primary/90 text-white py-3 px-8 rounded-lg font-medium btn-hover">開始新游戲</button></div></div></div><!-- AI思考提示 --><div id="aiThinking" class="fixed inset-0 bg-black/30 flex items-center justify-center z-40 hidden"><div class="bg-white rounded-xl p-6 max-w-xs w-full mx-4 flex items-center"><div class="mr-4"><i class="fa-solid fa-circle-notch fa-spin text-2xl text-primary"></i></div><div><h3 class="font-bold text-lg">AI思考中</h3><p class="text-gray-600 text-sm">電腦正在思考下一步...</p></div></div></div><script>document.addEventListener('DOMContentLoaded', () => {// 游戲常量const BOARD_SIZE = 15; // 15x15的棋盤const CELL_SIZE = Math.min(window.innerWidth * 0.8 / BOARD_SIZE, window.innerHeight * 0.6 / BOARD_SIZE);const PIECE_SIZE = CELL_SIZE * 0.8;// 游戲狀態let gameBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));let currentPlayer = 1; // 1: 黑棋, 2: 白棋let gameActive = true;let moveHistory = [];let gameTime = 0;let timerInterval;let isAiMode = true; // 默認開啟AI模式let playerPiece = 1; // 玩家默認執黑// DOM元素const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');const statusText = document.getElementById('statusText');const currentPlayerEl = document.getElementById('currentPlayer');const playerText = document.getElementById('playerText');const moveCountEl = document.getElementById('moveCount');const gameTimeEl = document.getElementById('gameTime');const restartBtn = document.getElementById('restartBtn');const undoBtn = document.getElementById('undoBtn');const winModal = document.getElementById('winModal');const winnerText = document.getElementById('winnerText');const newGameBtn = document.getElementById('newGameBtn');const aiBtn = document.getElementById('aiBtn');const humanBtn = document.getElementById('humanBtn');const gameMode = document.getElementById('gameMode');const modeText = document.getElementById('modeText');const aiThinking = document.getElementById('aiThinking');// 設置Canvas尺寸canvas.width = CELL_SIZE * (BOARD_SIZE - 1);canvas.height = CELL_SIZE * (BOARD_SIZE - 1);// 繪制棋盤function drawBoard() {ctx.clearRect(0, 0, canvas.width, canvas.height);// 繪制網格線ctx.strokeStyle = '#8B4513';ctx.lineWidth = 1.5;for (let i = 0; i < BOARD_SIZE; i++) {// 水平線ctx.beginPath();ctx.moveTo(0, i * CELL_SIZE);ctx.lineTo(canvas.width, i * CELL_SIZE);ctx.stroke();// 垂直線ctx.beginPath();ctx.moveTo(i * CELL_SIZE, 0);ctx.lineTo(i * CELL_SIZE, canvas.height);ctx.stroke();}// 繪制天元和星位const starPoints = [{x: 3, y: 3}, {x: 3, y: 11}, {x: 7, y: 7}, {x: 11, y: 3}, {x: 11, y: 11}];starPoints.forEach(point => {ctx.beginPath();ctx.arc(point.x * CELL_SIZE, point.y * CELL_SIZE, 4, 0, Math.PI * 2);ctx.fillStyle = '#8B4513';ctx.fill();});// 繪制棋子for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] !== 0) {drawPiece(i, j, gameBoard[i][j]);}}}}// 繪制棋子function drawPiece(row, col, player) {const x = col * CELL_SIZE;const y = row * CELL_SIZE;// 棋子陰影ctx.beginPath();ctx.arc(x, y, PIECE_SIZE / 2 + 2, 0, Math.PI * 2);ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';ctx.fill();// 棋子本體ctx.beginPath();ctx.arc(x, y, PIECE_SIZE / 2, 0, Math.PI * 2);if (player === 1) {// 黑棋 - 漸變效果const gradient = ctx.createRadialGradient(x - PIECE_SIZE / 6, y - PIECE_SIZE / 6, PIECE_SIZE / 10,x, y, PIECE_SIZE / 2);gradient.addColorStop(0, '#555');gradient.addColorStop(1, '#000');ctx.fillStyle = gradient;} else if (player === 2) {// 白棋 - 漸變效果const gradient = ctx.createRadialGradient(x - PIECE_SIZE / 6, y - PIECE_SIZE / 6, PIECE_SIZE / 10,x, y, PIECE_SIZE / 2);gradient.addColorStop(0, '#fff');gradient.addColorStop(1, '#ddd');ctx.fillStyle = gradient;}ctx.fill();// 棋子邊緣ctx.strokeStyle = player === 1 ? '#333' : '#ccc';ctx.lineWidth = 1;ctx.stroke();}// 檢查勝利條件function checkWin(row, col, player) {const directions = [[1, 0],   // 水平[0, 1],   // 垂直[1, 1],   // 對角線[1, -1]   // 反對角線];for (const [dx, dy] of directions) {let count = 1;  // 當前位置已經有一個棋子// 正向檢查for (let i = 1; i < 5; i++) {const newRow = row + i * dy;const newCol = col + i * dx;if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {break;}if (gameBoard[newRow][newCol] === player) {count++;} else {break;}}// 反向檢查for (let i = 1; i < 5; i++) {const newRow = row - i * dy;const newCol = col - i * dx;if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {break;}if (gameBoard[newRow][newCol] === player) {count++;} else {break;}}if (count >= 5) {return true;}}return false;}// 檢查平局function checkDraw() {for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] === 0) {return false; // 還有空位,不是平局}}}return true; // 棋盤已滿,平局}// 更新游戲狀態顯示function updateGameStatus() {if (gameActive) {if (isAiMode && currentPlayer !== playerPiece) {statusText.textContent = `游戲進行中 - AI思考中...`;currentPlayerEl.className = `w-6 h-6 rounded-full ${currentPlayer === 1 ? 'bg-black' : 'bg-white border border-gray-300'} mr-2 piece-shadow pulse-animation`;playerText.textContent = 'AI';} else {statusText.textContent = `游戲進行中 - ${currentPlayer === 1 ? '黑棋' : '白棋'}回合`;currentPlayerEl.className = `w-6 h-6 rounded-full ${currentPlayer === 1 ? 'bg-black' : 'bg-white border border-gray-300'} mr-2 piece-shadow`;playerText.textContent = currentPlayer === 1 ? '黑棋' : '白棋';}}moveCountEl.textContent = moveHistory.length;}// 更新游戲時間function updateGameTime() {gameTime++;const minutes = Math.floor(gameTime / 60).toString().padStart(2, '0');const seconds = (gameTime % 60).toString().padStart(2, '0');gameTimeEl.textContent = `${minutes}:${seconds}`;}// 開始計時function startTimer() {clearInterval(timerInterval);timerInterval = setInterval(updateGameTime, 1000);}// 停止計時function stopTimer() {clearInterval(timerInterval);}// 顯示勝利模態框function showWinModal(winner) {gameActive = false;stopTimer();let winnerTextContent = '';if (isAiMode) {winnerTextContent = winner === playerPiece ? '恭喜您獲勝!' : 'AI獲勝!';} else {winnerTextContent = `${winner === 1 ? '黑棋' : '白棋'}獲勝!`;}winnerText.textContent = winnerTextContent;winModal.classList.remove('hidden');// 添加動畫效果setTimeout(() => {winModal.classList.add('opacity-100');winModal.querySelector('div').classList.remove('scale-95');winModal.querySelector('div').classList.add('scale-100');}, 10);}// 隱藏勝利模態框function hideWinModal() {winModal.classList.remove('opacity-100');winModal.querySelector('div').classList.remove('scale-100');winModal.querySelector('div').classList.add('scale-95');setTimeout(() => {winModal.classList.add('hidden');}, 300);}// 重置游戲function resetGame() {gameBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));currentPlayer = 1;gameActive = true;moveHistory = [];gameTime = 0;drawBoard();updateGameStatus();gameTimeEl.textContent = '00:00';stopTimer();startTimer();hideWinModal();// 如果是AI模式且玩家執白,讓AI先行if (isAiMode && playerPiece === 2) {setTimeout(makeAiMove, 500);}}// 悔棋function undoMove() {if (moveHistory.length === 0 || !gameActive) {return;}// 如果是AI模式,需要撤銷兩步(玩家一步,AI一步)if (isAiMode && moveHistory.length >= 2) {for (let i = 0; i < 2; i++) {const lastMove = moveHistory.pop();gameBoard[lastMove.row][lastMove.col] = 0;currentPlayer = lastMove.player; // 回到上一個玩家}} else if (!isAiMode) {const lastMove = moveHistory.pop();gameBoard[lastMove.row][lastMove.col] = 0;currentPlayer = lastMove.player; // 回到上一個玩家}drawBoard();updateGameStatus();}// 顯示AI思考提示function showAiThinking() {aiThinking.classList.remove('hidden');setTimeout(() => {aiThinking.classList.add('opacity-100');}, 10);}// 隱藏AI思考提示function hideAiThinking() {aiThinking.classList.remove('opacity-100');setTimeout(() => {aiThinking.classList.add('hidden');}, 300);}// AI落子function makeAiMove() {if (!gameActive || !isAiMode || currentPlayer === playerPiece) {return;}showAiThinking();// 模擬AI思考時間setTimeout(() => {const aiPiece = playerPiece === 1 ? 2 : 1;const bestMove = findBestMove(aiPiece);if (bestMove) {const { row, col } = bestMove;// 落子gameBoard[row][col] = aiPiece;moveHistory.push({ row, col, player: aiPiece });// 添加落子動畫效果drawBoard();// 檢查是否勝利if (checkWin(row, col, aiPiece)) {hideAiThinking();showWinModal(aiPiece);return;}// 檢查是否平局if (checkDraw()) {gameActive = false;stopTimer();statusText.textContent = '游戲結束 - 平局!';hideAiThinking();return;}// 切換玩家currentPlayer = playerPiece;updateGameStatus();hideAiThinking();}}, 800);}// AI評估函數 - 評估棋盤狀態的分數function evaluateBoard(player) {const opponent = player === 1 ? 2 : 1;let score = 0;// 檢查所有可能的5連位置for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] === 0) {continue;}// 四個方向檢查const directions = [[1, 0],   // 水平[0, 1],   // 垂直[1, 1],   // 對角線[1, -1]   // 反對角線];for (const [dx, dy] of directions) {// 檢查是否會超出邊界if (i + 4 * dy >= BOARD_SIZE || i + 4 * dy < 0 || j + 4 * dx >= BOARD_SIZE || j + 4 * dx < 0) {continue;}// 計算當前方向的連子數let count = 1;let emptySpaces = 0;let blockedEnds = 0;// 正向檢查for (let k = 1; k < 5; k++) {const row = i + k * dy;const col = j + k * dx;if (gameBoard[row][col] === gameBoard[i][j]) {count++;} else if (gameBoard[row][col] === 0) {emptySpaces++;break;} else {blockedEnds++;break;}}// 反向檢查for (let k = 1; k < 5; k++) {const row = i - k * dy;const col = j - k * dx;if (gameBoard[row][col] === gameBoard[i][j]) {count++;} else if (gameBoard[row][col] === 0) {emptySpaces++;break;} else {blockedEnds++;break;}}// 如果兩端都被堵,這條線無效if (blockedEnds === 2) {continue;}// 根據連子數和開放端計算分數const isPlayerPiece = gameBoard[i][j] === player;let lineScore = 0;if (count >= 5) {lineScore = isPlayerPiece ? 10000 : -10000; // 勝利} else if (count === 4) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 1000 : -1000; // 活四} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 100 : -100; // 沖四}} else if (count === 3) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 100 : -100; // 活三} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 10 : -10; // 沖三}} else if (count === 2) {if (emptySpaces === 2) {lineScore = isPlayerPiece ? 5 : -5; // 活二} else if (emptySpaces === 1) {lineScore = isPlayerPiece ? 1 : -1; // 沖二}}score += lineScore;}}}return score;}// AI尋找最佳落子位置function findBestMove(player) {const opponent = player === 1 ? 2 : 1;let bestScore = -Infinity;let bestMove = null;// 只考慮已有棋子周圍的空位const possibleMoves = getPossibleMoves();for (const { row, col } of possibleMoves) {// 嘗試落子gameBoard[row][col] = player;// 檢查是否直接獲勝if (checkWin(row, col, player)) {gameBoard[row][col] = 0; // 恢復棋盤return { row, col };}// 評估這個位置的分數let score = evaluateBoard(player);// 恢復棋盤gameBoard[row][col] = 0;// 更新最佳移動if (score > bestScore) {bestScore = score;bestMove = { row, col };}}// 如果沒有找到最佳移動,隨機選擇一個空位if (!bestMove && possibleMoves.length > 0) {const randomIndex = Math.floor(Math.random() * possibleMoves.length);return possibleMoves[randomIndex];}// 如果棋盤為空,選擇中心點if (!bestMove) {return { row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) };}return bestMove;}// 獲取所有可能的落子位置(已有棋子周圍的空位)function getPossibleMoves() {const moves = [];const directions = [[0, 1], [1, 0], [1, 1], [1, -1],[0, -1], [-1, 0], [-1, -1], [-1, 1]];// 先檢查是否有任何棋子let hasPieces = false;for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] !== 0) {hasPieces = true;break;}}if (hasPieces) break;}// 如果棋盤為空,返回中心點if (!hasPieces) {return [{ row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) }];}// 檢查已有棋子周圍的空位for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] !== 0) {// 檢查周圍8個方向for (const [dx, dy] of directions) {const newRow = i + dy;const newCol = j + dx;// 檢查是否在棋盤內且為空if (newRow >= 0 && newRow < BOARD_SIZE && newCol >= 0 && newCol < BOARD_SIZE && gameBoard[newRow][newCol] === 0) {// 避免重復添加const isDuplicate = moves.some(move => move.row === newRow && move.col === newCol);if (!isDuplicate) {moves.push({ row: newRow, col: newCol });}}}}}}// 如果沒有找到周圍的空位,返回所有空位if (moves.length === 0) {for (let i = 0; i < BOARD_SIZE; i++) {for (let j = 0; j < BOARD_SIZE; j++) {if (gameBoard[i][j] === 0) {moves.push({ row: i, col: j });}}}}return moves;}// 點擊棋盤事件function handleCanvasClick(e) {if (!gameActive) return;// 如果是AI模式且當前是AI回合,不允許玩家落子if (isAiMode && currentPlayer !== playerPiece) {return;}const rect = canvas.getBoundingClientRect();const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;// 計算點擊的格子坐標const x = (e.clientX - rect.left) * scaleX;const y = (e.clientY - rect.top) * scaleY;const col = Math.round(x / CELL_SIZE);const row = Math.round(y / CELL_SIZE);// 檢查坐標是否在棋盤內且為空if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && gameBoard[row][col] === 0) {// 落子gameBoard[row][col] = currentPlayer;moveHistory.push({ row, col, player: currentPlayer });// 添加落子動畫效果drawBoard();// 檢查是否勝利if (checkWin(row, col, currentPlayer)) {showWinModal(currentPlayer);return;}// 檢查是否平局if (checkDraw()) {gameActive = false;stopTimer();statusText.textContent = '游戲結束 - 平局!';return;}// 切換玩家currentPlayer = currentPlayer === 1 ? 2 : 1;updateGameStatus();// 如果是AI模式且現在是AI回合,讓AI落子if (isAiMode && currentPlayer !== playerPiece) {setTimeout(makeAiMove, 500);}}}// 切換到人機對戰模式function switchToAiMode() {isAiMode = true;playerPiece = 1; // 默認玩家執黑modeText.textContent = '人機對戰';gameMode.querySelector('div').className = 'w-3 h-3 rounded-full bg-green-500 mr-2';aiBtn.className = 'flex-1 bg-green-500 hover:bg-green-600 text-white py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center';humanBtn.className = 'flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center';resetGame();}// 切換到人人對戰模式function switchToHumanMode() {isAiMode = false;modeText.textContent = '人人對戰';gameMode.querySelector('div').className = 'w-3 h-3 rounded-full bg-blue-500 mr-2';aiBtn.className = 'flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center';humanBtn.className = 'flex-1 bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-medium btn-hover flex items-center justify-center';resetGame();}// 事件監聽canvas.addEventListener('click', handleCanvasClick);restartBtn.addEventListener('click', resetGame);undoBtn.addEventListener('click', undoMove);newGameBtn.addEventListener('click', resetGame);aiBtn.addEventListener('click', switchToAiMode);humanBtn.addEventListener('click', switchToHumanMode);// 鼠標懸停預覽效果canvas.addEventListener('mousemove', (e) => {if (!gameActive) return;// 如果是AI模式且當前是AI回合,不顯示預覽if (isAiMode && currentPlayer !== playerPiece) {return;}const rect = canvas.getBoundingClientRect();const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;// 計算鼠標所在的格子坐標const x = (e.clientX - rect.left) * scaleX;const y = (e.clientY - rect.top) * scaleY;const col = Math.round(x / CELL_SIZE);const row = Math.round(y / CELL_SIZE);// 清除之前的預覽drawBoard();// 如果坐標在棋盤內且為空,繪制預覽棋子if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && gameBoard[row][col] === 0) {ctx.beginPath();ctx.arc(col * CELL_SIZE, row * CELL_SIZE, PIECE_SIZE / 2, 0, Math.PI * 2);if (currentPlayer === 1) {ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';} else {ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';}ctx.fill();}});// 鼠標離開棋盤時重繪canvas.addEventListener('mouseleave', () => {drawBoard();});// 初始化游戲drawBoard();updateGameStatus();startTimer();});</script>
</body>
</html>

總結

通過 HTML、CSS 和 JavaScript 的組合,實現了一個功能豐富的五子棋人機對戰游戲。這個游戲不僅具有基本的五子棋玩法,還通過 AI 對戰功能增加了游戲的趣味性和挑戰性。在開發過程中,我們學習了如何使用?canvas?進行圖形繪制,如何處理用戶交互事件,以及如何實現簡單的 AI 算法。希望這篇文章能對你有所幫助,讓你在 Web 開發的道路上更進一步。

你可以根據自己的需求對游戲進行擴展和優化,例如添加更復雜的 AI 算法、改進界面設計、增加游戲音效等。祝你在開發過程中取得成功!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/86981.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/86981.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/86981.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【QT】自動更新庫QSimpleUpdater使用實例封裝

【QT】自動更新庫QSimpleUpdater使用實例封裝 QSimpleUpdater 庫信號介紹appcastDownloaded 信號downloadFinished信號概括 參數介紹 實例編寫 QSimpleUpdater 庫 QSimpleUpdater是一個用于QT的開源自動更新庫&#xff0c;它可以幫助開發者實現應用程序的版本檢查和自動更新功…

Nginx、CDN、 DNS的關系解析

文章目錄 Nginx 與 CDN 的關系1. 角色定位2. 協作方式3. 自建 CDN vs. 第三方 CDN Nginx 與 DNS 的關系1. 角色定位2. 協作方式3. 性能優化 CDN 與 DNS 的關系1. 角色定位2. 協作方式3. 高級 DNS 技術 三者結合的典型架構總結 Nginx、CDN 和 DNS 是現代網絡架構中的三個關鍵組件…

PH熱榜 | 2025-06-13

1. Flowstep 標語&#xff1a;借助人工智能&#xff0c;瞬間設計出美觀的用戶界面。 介紹&#xff1a;Flowstep 是您的 AI 設計助手&#xff0c;將簡單的文字提示轉化為用戶界面設計、線框圖和流程圖。它能加速您的工作流程&#xff0c;讓您輕松迭代&#xff0c;內置建議幫助…

Spectacular AI Gemini2 跑通實時建圖

參考鏈接&#xff1a;Spectacular AI 硬件設備 gemini2 測試了gemini335沒成功 修改record.cpp 倉庫鏈接&#xff1a;sdk 讀取Timu_cam ros2 run tf2_ros tf2_echo imu坐標系&#xff08;加速度計和陀螺儀都可以&#xff0c;兩者變換為單位陣&#xff09;camera_rgb_optic…

簡析MDM在餐飲設備中的部署與應用

引言&#xff1a;科技驅動餐飲運營升級 在競爭激烈的餐飲行業&#xff0c;科技已成為提升服務質量和運營效率的關鍵。從自助點餐機、數字菜單牌&#xff0c;到移動收銀系統&#xff0c;智能設備已經深入餐廳的日常運營。然而&#xff0c;隨著設備數量和種類的增加&#xff0c;…

RocketMQ總結

深入理解RocketMQ三高架構設計 高性能 順序寫磁盤 mmap 零拷貝異步刷盤 刷盤策略可配置輕量網絡協議 長連接復用 高可用 主從復制機制、controller、dledger集群NameServer 多副本無狀態客戶端自動切換 Broker消息刷盤機制保障可靠性 高可擴展性 Broker 水平擴展Consu…

vue3+tdesign實現明暗主題切換

很多項目中有些會涉及到明暗主題切換的功能&#xff0c;今天就來梳理一下有關這方面的具體實現步驟和使用到的方法&#xff0c;本篇文章使用到的組件是tdesign&#xff0c;利用部分案例&#xff0c;主要闡述明暗主題切換的技術原理和實現步驟&#xff0c;僅供參考。 目錄 一、…

goland 的 dug 設置

brew install delve # 或通過go install go install github.com/go-delve/delve/cmd/dlvlatestwhere dlvdlv.path/your/path/to/dlv環境變量 DEPLOY_MODEprivate;EGO_DEBUGtrue;EGO_MODEdev;MO_MODEdev;PD_MODEco;PRODUCT_MODEall

CVE-2020-1938源碼分析與漏洞復現(Tomcat 文件包含/讀取)

漏洞概述 漏洞名稱&#xff1a;Tomcat AJP協議文件包含/讀取漏洞&#xff08;Ghostcat&#xff09; CVE 編號&#xff1a;CVE-2020-1938 CVSS 評分&#xff1a;9.8 影響版本&#xff1a; Apache Tomcat 6.x (≤ 6.0.53)Apache Tomcat 7.x (≤ 7.0.99)Apache Tomcat 8.x (≤ 8…

基于51單片機的簡易打鈴系統

目錄 具體實現功能 設計介紹 資料內容 全部內容 資料獲取 具體實現功能 具體功能&#xff1a; &#xff08;1&#xff09;實時顯示當前時間&#xff08;時、分、秒&#xff09;&#xff0c;LED模式指示燈亮&#xff1b;4個按鍵可以調整時間的時和分。 &#xff08;2&…

vue+cesium示例:3D熱力圖(附源碼下載)

接到一位知識星友的邀請&#xff0c;隨機模擬三維數據點&#xff0c;結合heatmap.js實現基于cesiumvue的3D熱力圖需求&#xff0c;適合學習Cesium與前端框架結合開發3D可視化項目。 demo源碼運行環境以及配置 運行環境&#xff1a;依賴Node安裝環境&#xff0c;demo本地Node版本…

批處理實現:自動抓取perfetto日志 自動導出到當前文件夾 自動打開分析頁面

序言 最近在研究性能問題需要抓取trace文件。于是寫了個腳本 使用 雙擊運行批處理文件&#xff0c;可以開始記錄trace。而且以當前文件夾下面的。config.pbtx 作為配置文件。 &#xff08;pbtx就是一個json文件。配置了需要抓取那些參數&#xff0c;可以通過https://ui.per…

未來機器人的大腦:如何用神經網絡模擬器實現更智能的決策?

編輯&#xff1a;陳萍萍的公主一點人工一點智能 未來機器人的大腦&#xff1a;如何用神經網絡模擬器實現更智能的決策&#xff1f;RWM通過雙自回歸機制有效解決了復合誤差、部分可觀測性和隨機動力學等關鍵挑戰&#xff0c;在不依賴領域特定歸納偏見的條件下實現了卓越的預測準…

??Promise代碼理解

1.事件循環與 Promise 執行順序 案例 1&#xff1a;基礎 Promise 同步異步區分 console.log(1); new Promise(resolve > {console.log(2);resolve();console.log(3); }).then(() > console.log(4)); console.log(5); 輸出順序&#xff1a;1&#xff0c;2&#xff0c;3…

57、原生組件注入-【源碼分析】DispatcherServlet注入原理

57、原生組件注入DispatcherServlet注入原理 #### 繼承關系 - DispatcherServlet繼承自FrameworkServlet&#xff0c;而FrameworkServlet繼承自HttpServletBean&#xff0c;最終HttpServletBean繼承自HttpServlet。 - DispatcherServlet實現了ApplicationContextAware接口。 …

【動手學深度學習】3.5. 圖像分類數據集

目錄 3.5. 圖像分類數據集1&#xff09;讀取數據集2&#xff09;讀取小批量3&#xff09;整合所有組件4&#xff09;小結 . 3.5. 圖像分類數據集 我們將使用Fashion-MNIST數據集&#xff0c;作為圖像分類數據集。 %matplotlib inline import torch import torchvision from …

Python的格式化輸入輸出

# Python 的格式化輸出和格式化輸入## 格式化輸出Python 提供了多種字符串格式化的方法&#xff1a;### 1. % 格式化&#xff08;舊式格式化&#xff09;python name "Alice" age 25 print("Name: %s, Age: %d" % (name, age)) # 輸出: Name: Alice, Age…

day65—回溯—單詞搜索(LeetCode-79)

題目描述 給定一個 m x n 二維字符網格 board 和一個字符串單詞 word 。如果 word 存在于網格中&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 單詞必須按照字母順序&#xff0c;通過相鄰的單元格內的字母構成&#xff0c;其中“相鄰”單元格是那些水平相…

iOS安全和逆向系列教程 第19篇:ARM64匯編語言基礎與逆向分析

引言 在成功掌握iOS應用脫殼技術后,我們獲得了可以進行靜態分析的二進制文件。然而,要真正理解iOS應用的底層邏輯,我們必須深入到匯編語言層面。ARM64(也稱為AArch64)是蘋果在iPhone 5s及以后設備中使用的指令集架構。本篇文章將深入探討ARM64匯編語言的基礎知識,并結合…

使用Gitlab CI/CD結合docker容器實現自動化部署

Gitlab CI/CD基本介紹 核心概念 持續集成&#xff08;CI&#xff09;&#xff1a;每次代碼提交后自動觸發構建、測試和代碼檢查&#xff0c;確保代碼質量 持續交付/部署&#xff08;CD&#xff09;&#xff1a;在 CI 基礎上自動將代碼部署到測試或生產環境&#xff0c;支持人工…