目录
第一版:双人对战
执行代码
<!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 {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
canvas {
border: 1px solid black;
}
#status {
margin-top: 10px;
font-size: 18px;
}
#restart {
margin-top: 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="board" width="450" height="450"></canvas>
<div id="status">当前玩家:黑棋</div>
<button id="restart">重新开始</button>
<script>
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
const restartBtn = document.getElementById('restart');
const size = 15; // 15x15 棋盘
const cellSize = 30; // 每个格子大小
const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
let currentPlayer = 1; // 1:黑棋, 2:白棋
let gameOver = false;
// 绘制棋盘
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
for (let i = 0; i < size; i++) {
// 画横线
ctx.moveTo(0, i * cellSize);
ctx.lineTo(canvas.width, i * cellSize);
// 画竖线
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, canvas.height);
}
ctx.strokeStyle = '#000';
ctx.stroke();
// 绘制棋子
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 1) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
} else if (board[i][j] === 2) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.fill();
ctx.stroke();
}
}
}
}
// 检查胜利
function checkWin(row, col, player) {
const directions = [
[0, 1], [1, 0], [1, 1], [1, -1] // 水平、垂直、斜线
];
for (let [dx, dy] of directions) {
let count = 1;
// 正方向
for (let i = 1; i < 5; i++) {
let newRow = row + i * dx;
let newCol = col + i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
// 反方向
for (let i = 1; i < 5; i++) {
let newRow = row - i * dx;
let newCol = col - i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
if (count >= 5) return true;
}
return false;
}
// 处理点击
canvas.addEventListener('click', (e) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const row = Math.round(y / cellSize);
const col = Math.round(x / cellSize);
if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
board[row][col] = currentPlayer;
drawBoard();
if (checkWin(row, col, currentPlayer)) {
status.textContent = `游戏结束!${currentPlayer === 1 ? '黑棋' : '白棋'}获胜!`;
gameOver = true;
} else {
currentPlayer = currentPlayer === 1 ? 2 : 1;
status.textContent = `当前玩家:${currentPlayer === 1 ? '黑棋' : '白棋'}`;
}
}
});
// 重新开始
restartBtn.addEventListener('click', () => {
board.forEach(row => row.fill(0));
currentPlayer = 1;
gameOver = false;
status.textContent = '当前玩家:黑棋';
drawBoard();
});
// 初始化棋盘
drawBoard();
</script>
</body>
</html>
运行结果
第二版本:人机对战
执行代码
<!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 {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
canvas {
border: 1px solid black;
}
#status {
margin-top: 10px;
font-size: 18px;
}
#timer {
margin-top: 5px;
font-size: 16px;
color: red;
}
#restart {
margin-top: 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="board" width="450" height="450"></canvas>
<div id="status">当前玩家:黑棋(玩家)</div>
<div id="timer">剩余时间:20秒</div>
<button id="restart">重新开始</button>
<script>
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
const timerDisplay = document.getElementById('timer');
const restartBtn = document.getElementById('restart');
const size = 15; // 15x15 棋盘
const cellSize = 30; // 每个格子大小
const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
let gameOver = false;
let timer;
let timeLeft = 20; // 玩家20秒,AI 5秒
// 绘制棋盘
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
for (let i = 0; i < size; i++) {
ctx.moveTo(0, i * cellSize);
ctx.lineTo(canvas.width, i * cellSize);
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, canvas.height);
}
ctx.strokeStyle = '#000';
ctx.stroke();
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 1) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
} else if (board[i][j] === 2) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.fill();
ctx.stroke();
}
}
}
}
// 检查胜利
function checkWin(row, col, player) {
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
for (let i = 1; i < 5; i++) {
let newRow = row + i * dx;
let newCol = col + i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
let newRow = row - i * dx;
let newCol = col - i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
if (count >= 5) return true;
}
return false;
}
// 评估棋盘得分
function evaluateBoard() {
let score = 0;
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) continue;
const player = board[i][j];
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
let openEnds = 0;
for (let k = 1; k < 5; k++) {
let newRow = i + k * dx;
let newCol = j + k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
for (let k = 1; k < 5; k++) {
let newRow = i - k * dx;
let newCol = j - k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
if (count >= 5) return player === 2 ? Infinity : -Infinity;
if (player === 2) {
if (count === 4 && openEnds >= 1) score += 10000;
else if (count === 3 && openEnds === 2) score += 1000;
else if (count === 3 && openEnds === 1) score += 100;
else if (count === 2 && openEnds === 2) score += 10;
} else {
if (count === 4 && openEnds >= 1) score -= 12000;
else if (count === 3 && openEnds === 2) score -= 1200;
else if (count === 3 && openEnds === 1) score -= 120;
else if (count === 2 && openEnds === 2) score -= 12;
}
}
}
}
return score;
}
// 获取可落子位置(优化:仅考虑邻近棋子的空位)
function getValidMoves() {
const moves = [];
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) {
let hasNeighbor = false;
for (let di = -1; di <= 1; di++) {
for (let dj = -1; dj <= 1; dj++) {
let ni = i + di, nj = j + dj;
if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
hasNeighbor = true;
break;
}
}
if (hasNeighbor) break;
}
if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
moves.push([i, j]);
}
}
}
}
return moves;
}
// Minimax 算法 + Alpha-Beta 剪枝
function minimax(depth, alpha, beta, isMaximizing) {
if (depth === 0) return evaluateBoard();
let validMoves = getValidMoves();
if (validMoves.length === 0) return evaluateBoard();
if (isMaximizing) {
let maxEval = -Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 2;
if (checkWin(row, col, 2)) {
board[row][col] = 0;
return Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, false);
board[row][col] = 0;
maxEval = Math.max(maxEval, evalScore);
alpha = Math.max(alpha, evalScore);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 1;
if (checkWin(row, col, 1)) {
board[row][col] = 0;
return -Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, true);
board[row][col] = 0;
minEval = Math.min(minEval, evalScore);
beta = Math.min(beta, evalScore);
if (beta <= alpha) break;
}
return minEval;
}
}
// AI 选择最佳落子
function aiMove() {
let bestScore = -Infinity;
let bestMove = null;
const validMoves = getValidMoves();
const depth = 2;
const startTime = performance.now();
for (let [row, col] of validMoves) {
board[row][col] = 2;
let score = minimax(depth, -Infinity, Infinity, false);
board[row][col] = 0;
if (score > bestScore) {
bestScore = score;
bestMove = [row, col];
}
if (performance.now() - startTime > 4500) break; // 限制 AI 计算时间
}
if (bestMove) {
const [row, col] = bestMove;
board[row][col] = 2;
drawBoard();
clearInterval(timer);
if (checkWin(row, col, 2)) {
status.textContent = '游戏结束!白棋(AI)获胜!';
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 1;
status.textContent = '当前玩家:黑棋(玩家)';
startTimer(20); // 玩家 20 秒
}
} else {
// AI 超时或无合法落子
clearInterval(timer);
status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
// 计时器
function startTimer(seconds) {
timeLeft = seconds;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
clearInterval(timer);
timer = setInterval(() => {
timeLeft--;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
if (timeLeft <= 0) {
clearInterval(timer);
if (currentPlayer === 1) {
status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
timerDisplay.textContent = '';
gameOver = true;
} else {
status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
}, 1000);
}
// 处理玩家点击
canvas.addEventListener('click', (e) => {
if (gameOver || currentPlayer !== 1) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const row = Math.round(y / cellSize);
const col = Math.round(x / cellSize);
if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
board[row][col] = 1;
drawBoard();
clearInterval(timer);
if (checkWin(row, col, 1)) {
status.textContent = '游戏结束!黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 2;
status.textContent = 'AI(白棋)思考中...';
timerDisplay.textContent = '剩余时间:5秒';
startTimer(5); // AI 5 秒
setTimeout(() => {
aiMove();
}, 500);
}
}
});
// 重新开始
restartBtn.addEventListener('click', () => {
board.forEach(row => row.fill(0));
currentPlayer = 1;
gameOver = false;
status.textContent = '当前玩家:黑棋(玩家)';
timerDisplay.textContent = '剩余时间:20秒';
clearInterval(timer);
drawBoard();
startTimer(20); // 玩家 20 秒
});
// 初始化棋盘并启动计时器
drawBoard();
startTimer(20); // 玩家 20 秒
</script>
</body>
</html>
优化点
- 玩家超时(20 秒未落子)会导致 AI 获胜,AI 超时(5 秒未落子)会导致玩家获胜。
- AI 使用 minimax 算法(深度 2)结合 alpha-beta 剪枝,计算时间受限以确保 5 秒内完成。
- 计时器通过 setInterval 实现,每秒更新,AI 计算时间通过 performance.now() 监控,限制在 4.5 秒内以留出余量。
第三版:人机对战
执行代码
<!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 {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
canvas {
border: 1px solid black;
}
#status {
margin-top: 10px;
font-size: 18px;
}
#timer {
margin-top: 5px;
font-size: 16px;
color: red;
}
#buttons {
margin-top: 10px;
}
#undo, #restart {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
margin: 0 5px;
}
#undo:disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style>
</head>
<body>
<canvas id="board" width="450" height="450"></canvas>
<div id="status">当前玩家:黑棋(玩家)</div>
<div id="timer">剩余时间:20秒</div>
<div id="buttons">
<button id="undo">悔棋(剩余3次)</button>
<button id="restart">重新开始</button>
</div>
<script>
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
const timerDisplay = document.getElementById('timer');
const undoBtn = document.getElementById('undo');
const restartBtn = document.getElementById('restart');
const size = 15; // 15x15 棋盘
const cellSize = 30; // 每个格子大小
const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
let gameOver = false;
let timer;
let timeLeft = 20; // 玩家20秒,AI 5秒
let undoCount = 3; // 悔棋次数
let moveHistory = []; // 存储落子历史
let lastMove = null; // 最新落子位置
// 绘制棋盘
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
for (let i = 0; i < size; i++) {
ctx.moveTo(0, i * cellSize);
ctx.lineTo(canvas.width, i * cellSize);
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, canvas.height);
}
ctx.strokeStyle = '#000';
ctx.stroke();
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 1) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
} else if (board[i][j] === 2) {
ctx.beginPath();
ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.fill();
ctx.stroke();
}
}
}
// 高亮最新落子
if (lastMove) {
const [row, col] = lastMove;
ctx.beginPath();
ctx.rect(col * cellSize - cellSize / 2, row * cellSize - cellSize / 2, cellSize, cellSize);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineWidth = 1; // 重置线宽
setTimeout(() => {
drawBoard(); // 2秒后清除高亮
}, 2000);
}
}
// 检查胜利
function checkWin(row, col, player) {
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
for (let i = 1; i < 5; i++) {
let newRow = row + i * dx;
let newCol = col + i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
let newRow = row - i * dx;
let newCol = col - i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
if (count >= 5) return true;
}
return false;
}
// 评估棋盘得分
function evaluateBoard() {
let score = 0;
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) continue;
const player = board[i][j];
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
let openEnds = 0;
for (let k = 1; k < 5; k++) {
let newRow = i + k * dx;
let newCol = j + k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
for (let k = 1; k < 5; k++) {
let newRow = i - k * dx;
let newCol = j - k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
if (count >= 5) return player === 2 ? Infinity : -Infinity;
if (player === 2) {
if (count === 4 && openEnds >= 1) score += 10000;
else if (count === 3 && openEnds === 2) score += 1000;
else if (count === 3 && openEnds === 1) score += 100;
else if (count === 2 && openEnds === 2) score += 10;
} else {
if (count === 4 && openEnds >= 1) score -= 12000;
else if (count === 3 && openEnds === 2) score -= 1200;
else if (count === 3 && openEnds === 1) score -= 120;
else if (count === 2 && openEnds === 2) score -= 12;
}
}
}
}
return score;
}
// 获取可落子位置
function getValidMoves() {
const moves = [];
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) {
let hasNeighbor = false;
for (let di = -1; di <= 1; di++) {
for (let dj = -1; dj <= 1; dj++) {
let ni = i + di, nj = j + dj;
if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
hasNeighbor = true;
break;
}
}
if (hasNeighbor) break;
}
if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
moves.push([i, j]);
}
}
}
}
return moves;
}
// Minimax 算法 + Alpha-Beta 剪枝
function minimax(depth, alpha, beta, isMaximizing) {
if (depth === 0) return evaluateBoard();
let validMoves = getValidMoves();
if (validMoves.length === 0) return evaluateBoard();
if (isMaximizing) {
let maxEval = -Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 2;
if (checkWin(row, col, 2)) {
board[row][col] = 0;
return Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, false);
board[row][col] = 0;
maxEval = Math.max(maxEval, evalScore);
alpha = Math.max(alpha, evalScore);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 1;
if (checkWin(row, col, 1)) {
board[row][col] = 0;
return -Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, true);
board[row][col] = 0;
minEval = Math.min(minEval, evalScore);
beta = Math.min(beta, evalScore);
if (beta <= alpha) break;
}
return minEval;
}
}
// AI 选择最佳落子
function aiMove() {
let bestScore = -Infinity;
let bestMove = null;
const validMoves = getValidMoves();
const depth = 2;
const startTime = performance.now();
for (let [row, col] of validMoves) {
board[row][col] = 2;
let score = minimax(depth, -Infinity, Infinity, false);
board[row][col] = 0;
if (score > bestScore) {
bestScore = score;
bestMove = [row, col];
}
if (performance.now() - startTime > 4500) break;
}
if (bestMove) {
const [row, col] = bestMove;
board[row][col] = 2;
moveHistory.push({ player: 2, row, col });
lastMove = [row, col];
drawBoard();
clearInterval(timer);
status.textContent = `白棋(AI)落子:(${row}, ${col})`;
if (checkWin(row, col, 2)) {
status.textContent = `游戏结束!白棋(AI)获胜!`;
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 1;
setTimeout(() => {
status.textContent = '当前玩家:黑棋(玩家)';
startTimer(20);
}, 2000);
}
} else {
clearInterval(timer);
status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
// 计时器
function startTimer(seconds) {
timeLeft = seconds;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
clearInterval(timer);
timer = setInterval(() => {
timeLeft--;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
if (timeLeft <= 0) {
clearInterval(timer);
if (currentPlayer === 1) {
status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
timerDisplay.textContent = '';
gameOver = true;
} else {
status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
}, 1000);
}
// 处理玩家点击
canvas.addEventListener('click', (e) => {
if (gameOver || currentPlayer !== 1) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const row = Math.round(y / cellSize);
const col = Math.round(x / cellSize);
if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
board[row][col] = 1;
moveHistory.push({ player: 1, row, col });
lastMove = [row, col];
drawBoard();
clearInterval(timer);
status.textContent = `黑棋(玩家)落子:(${row}, ${col})`;
if (checkWin(row, col, 1)) {
status.textContent = `游戏结束!黑棋(玩家)获胜!`;
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 2;
setTimeout(() => {
status.textContent = 'AI(白棋)思考中...';
timerDisplay.textContent = '剩余时间:5秒';
startTimer(5);
setTimeout(() => {
aiMove();
}, 500);
}, 2000);
}
}
});
// 悔棋
undoBtn.addEventListener('click', () => {
if (gameOver || undoCount <= 0 || currentPlayer !== 1 || moveHistory.length < 2) return;
// 撤销最后两次落子(AI 和玩家各一次)
for (let i = 0; i < 2; i++) {
if (moveHistory.length > 0) {
const last = moveHistory.pop();
board[last.row][last.col] = 0;
}
}
undoCount--;
undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
if (undoCount === 0) undoBtn.disabled = true;
lastMove = moveHistory.length > 0 ? [moveHistory[moveHistory.length - 1].row, moveHistory[moveHistory.length - 1].col] : null;
drawBoard();
clearInterval(timer);
status.textContent = '当前玩家:黑棋(玩家)';
startTimer(20);
});
// 重新开始
restartBtn.addEventListener('click', () => {
board.forEach(row => row.fill(0));
currentPlayer = 1;
gameOver = false;
undoCount = 3;
moveHistory = [];
lastMove = null;
undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
undoBtn.disabled = false;
status.textContent = '当前玩家:黑棋(玩家)';
timerDisplay.textContent = '剩余时间:20秒';
clearInterval(timer);
drawBoard();
startTimer(20);
});
// 初始化棋盘并启动计时器
drawBoard();
startTimer(20);
</script>
</body>
</html>
运行结果
优化点
- 落子提示:最新落子以红色边框高亮 2 秒,状态栏显示落子坐标,确保玩家清楚落子位置。
- 悔棋限制:最多 3 次,需在玩家回合使用,撤销最近的玩家和 AI 落子,保持游戏公平。
第四版:人机对战
执行代码
<!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>
<style>
body {
background: linear-gradient(to bottom, #f0f4f8, #d9e2ec);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Arial', sans-serif;
}
canvas {
background: url('https://www.transparenttextures.com/patterns/wood-pattern.png');
border: 4px solid #4a3728;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
</style>
</head>
<body>
<div class="flex flex-col items-center p-6">
<h1 class="text-3xl font-bold text-gray-800 mb-4">五子棋 - 人机对战</h1>
<canvas id="board" width="480" height="480" class="mb-4"></canvas>
<div id="status" class="text-xl text-gray-700 mb-2">当前玩家:黑棋(玩家)</div>
<div id="timer" class="text-lg text-red-600 mb-4">剩余时间:20秒</div>
<div id="buttons" class="flex space-x-4">
<button id="undo" class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed">悔棋(剩余3次)</button>
<button id="restart" class="bg-green-500 text-white px-6 py-2 rounded-lg hover:bg-green-600">重新开始</button>
</div>
</div>
<script>
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
const timerDisplay = document.getElementById('timer');
const undoBtn = document.getElementById('undo');
const restartBtn = document.getElementById('restart');
const size = 15; // 15x15 棋盘
const cellSize = 30; // 每个格子大小
const offset = 30; // 边缘偏移,确保棋子完整显示
const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
let gameOver = false;
let timer;
let timeLeft = 20; // 玩家20秒,AI 5秒
let undoCount = 3; // 悔棋次数
let moveHistory = []; // 落子历史
let lastMove = null; // 最新落子
// 绘制棋盘
function drawBoard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
for (let i = 0; i < size; i++) {
// 横线
ctx.moveTo(offset, offset + i * cellSize);
ctx.lineTo(offset + (size - 1) * cellSize, offset + i * cellSize);
// 竖线
ctx.moveTo(offset + i * cellSize, offset);
ctx.lineTo(offset + i * cellSize, offset + (size - 1) * cellSize);
}
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
// 绘制棋子
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 1) {
ctx.beginPath();
ctx.arc(offset + j * cellSize, offset + i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
} else if (board[i][j] === 2) {
ctx.beginPath();
ctx.arc(offset + j * cellSize, offset + i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.fill();
ctx.stroke();
}
}
}
// 高亮最新落子
if (lastMove) {
const [row, col] = lastMove;
ctx.beginPath();
ctx.rect(offset + col * cellSize - cellSize / 2, offset + row * cellSize - cellSize / 2, cellSize, cellSize);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineWidth = 1;
setTimeout(() => {
drawBoard();
}, 2000);
}
}
// 检查胜利
function checkWin(row, col, player) {
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
for (let i = 1; i < 5; i++) {
let newRow = row + i * dx;
let newCol = col + i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
let newRow = row - i * dx;
let newCol = col - i * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
break;
}
}
if (count >= 5) return true;
}
return false;
}
// 评估棋盘得分
function evaluateBoard() {
let score = 0;
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) continue;
const player = board[i][j];
const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (let [dx, dy] of directions) {
let count = 1;
let openEnds = 0;
for (let k = 1; k < 5; k++) {
let newRow = i + k * dx;
let newCol = j + k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
for (let k = 1; k < 5; k++) {
let newRow = i - k * dx;
let newCol = j - k * dy;
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
count++;
} else {
if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
openEnds++;
}
break;
}
}
if (count >= 5) return player === 2 ? Infinity : -Infinity;
if (player === 2) {
if (count === 4 && openEnds >= 1) score += 10000;
else if (count === 3 && openEnds === 2) score += 1000;
else if (count === 3 && openEnds === 1) score += 100;
else if (count === 2 && openEnds === 2) score += 10;
} else {
if (count === 4 && openEnds >= 1) score -= 12000;
else if (count === 3 && openEnds === 2) score -= 1200;
else if (count === 3 && openEnds === 1) score -= 120;
else if (count === 2 && openEnds === 2) score -= 12;
}
}
}
}
return score;
}
// 获取可落子位置
function getValidMoves() {
const moves = [];
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (board[i][j] === 0) {
let hasNeighbor = false;
for (let di = -1; di <= 1; di++) {
for (let dj = -1; dj <= 1; dj++) {
let ni = i + di, nj = j + dj;
if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
hasNeighbor = true;
break;
}
}
if (hasNeighbor) break;
}
if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
moves.push([i, j]);
}
}
}
}
return moves;
}
// Minimax 算法 + Alpha-Beta 剪枝
function minimax(depth, alpha, beta, isMaximizing) {
if (depth === 0) return evaluateBoard();
let validMoves = getValidMoves();
if (validMoves.length === 0) return evaluateBoard();
if (isMaximizing) {
let maxEval = -Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 2;
if (checkWin(row, col, 2)) {
board[row][col] = 0;
return Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, false);
board[row][col] = 0;
maxEval = Math.max(maxEval, evalScore);
alpha = Math.max(alpha, evalScore);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (let [row, col] of validMoves) {
board[row][col] = 1;
if (checkWin(row, col, 1)) {
board[row][col] = 0;
return -Infinity;
}
let evalScore = minimax(depth - 1, alpha, beta, true);
board[row][col] = 0;
minEval = Math.min(minEval, evalScore);
beta = Math.min(beta, evalScore);
if (beta <= alpha) break;
}
return minEval;
}
}
// AI 选择最佳落子
function aiMove() {
let bestScore = -Infinity;
let bestMove = null;
const validMoves = getValidMoves();
const depth = 2;
const startTime = performance.now();
for (let [row, col] of validMoves) {
board[row][col] = 2;
let score = minimax(depth, -Infinity, Infinity, false);
board[row][col] = 0;
if (score > bestScore) {
bestScore = score;
bestMove = [row, col];
}
if (performance.now() - startTime > 4500) break;
}
if (bestMove) {
const [row, col] = bestMove;
board[row][col] = 2;
moveHistory.push({ player: 2, row, col });
lastMove = [row, col];
drawBoard();
clearInterval(timer);
status.textContent = `白棋(AI)落子:(${row}, ${col})`;
if (checkWin(row, col, 2)) {
status.textContent = `游戏结束!白棋(AI)获胜!`;
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 1;
setTimeout(() => {
status.textContent = '当前玩家:黑棋(玩家)';
startTimer(20);
}, 2000);
}
} else {
clearInterval(timer);
status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
// 计时器
function startTimer(seconds) {
timeLeft = seconds;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
clearInterval(timer);
timer = setInterval(() => {
timeLeft--;
timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
if (timeLeft <= 0) {
clearInterval(timer);
if (currentPlayer === 1) {
status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
timerDisplay.textContent = '';
gameOver = true;
} else {
status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
timerDisplay.textContent = '';
gameOver = true;
}
}
}, 1000);
}
// 处理玩家点击
canvas.addEventListener('click', (e) => {
if (gameOver || currentPlayer !== 1) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const row = Math.round((y - offset) / cellSize);
const col = Math.round((x - offset) / cellSize);
if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
board[row][col] = 1;
moveHistory.push({ player: 1, row, col });
lastMove = [row, col];
drawBoard();
clearInterval(timer);
status.textContent = `黑棋(玩家)落子:(${row}, ${col})`;
if (checkWin(row, col, 1)) {
status.textContent = `游戏结束!黑棋(玩家)获胜!`;
timerDisplay.textContent = '';
gameOver = true;
} else {
currentPlayer = 2;
setTimeout(() => {
status.textContent = 'AI(白棋)思考中...';
timerDisplay.textContent = '剩余时间:5秒';
startTimer(5);
setTimeout(() => {
aiMove();
}, 500);
}, 2000);
}
}
});
// 悔棋
undoBtn.addEventListener('click', () => {
if (gameOver || undoCount <= 0 || currentPlayer !== 1 || moveHistory.length < 2) return;
for (let i = 0; i < 2; i++) {
if (moveHistory.length > 0) {
const last = moveHistory.pop();
board[last.row][last.col] = 0;
}
}
undoCount--;
undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
if (undoCount === 0) undoBtn.disabled = true;
lastMove = moveHistory.length > 0 ? [moveHistory[moveHistory.length - 1].row, moveHistory[moveHistory.length - 1].col] : null;
drawBoard();
clearInterval(timer);
status.textContent = '当前玩家:黑棋(玩家)';
startTimer(20);
});
// 重新开始
restartBtn.addEventListener('click', () => {
board.forEach(row => row.fill(0));
currentPlayer = 1;
gameOver = false;
undoCount = 3;
moveHistory = [];
lastMove = null;
undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
undoBtn.disabled = false;
status.textContent = '当前玩家:黑棋(玩家)';
timerDisplay.textContent = '剩余时间:20秒';
clearInterval(timer);
drawBoard();
startTimer(20);
});
// 初始化棋盘并启动计时器
drawBoard();
startTimer(20);
</script>
</body>
</html>
运行结果
优化点
- 美观性:使用 Tailwind CSS 优化布局,添加渐变背景、木纹棋盘、按钮悬停效果,提升视觉体验。
- 边缘棋子:棋盘格线和棋子从偏移 30px 开始绘制,canvas 尺寸调整为 480x480,确保边缘棋子完整显示。