<!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>
:root {
--bg-color: #f5f6fa;
--board-color: #e4b980;
--line-color: #634d31;
--primary-color: #4a90e2;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 10px;
}
.container {
width: 100%;
max-width: 500px;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
h1 {
font-size: 1.5rem;
color: #333;
font-weight: 600;
}
.status {
font-size: 1.1rem;
font-weight: bold;
color: var(--primary-color);
height: 24px;
}
.board-wrapper {
width: 100%;
aspect-ratio: 1 / 1;
background-color: var(--board-color);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
padding: 12px;
position: relative;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: pointer;
}
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 24px;
font-size: 1rem;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
transition: all 0.2s ease;
}
.btn:active {
transform: scale(0.95);
box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
}
</style>
</head>
<body>
<div class="container">
<h1>五子棋 AI 对战</h1>
<div class="status" id="status-text">你是黑棋,请落子</div>
<div class="board-wrapper">
<canvas id="gobang"></canvas>
</div>
<button class="btn" onclick="restartGame()">重新开始</button>
</div>
<script>
const canvas = document.getElementById('gobang');
const ctx = canvas.getContext('2d');
const statusText = document.getElementById('status-text');
const GRID_SIZE = 15;
let cellSize = 0;
// ===== 修复点 1:直接初始化为 15×15 的二维零矩阵 =====
let board = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0));
let gameOver = false;
let isAiTurn = false;
let lastMove = null;
let haloAngle = 0;
function initCanvas() {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
cellSize = rect.width / (GRID_SIZE + 1);
// ===== 修复点 2:移除这里的 render(),避免在 board 未就绪时渲染 =====
// 渲染工作交给 restartGame() 或动画循环
}
function restartGame() {
board = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0));
gameOver = false;
isAiTurn = false;
lastMove = null;
statusText.innerText = "你是黑棋,请落子";
statusText.style.color = "#4a90e2";
render();
}
// 每一帧动画都重绘整个棋盘和现有棋子,确保落子持续显示
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. 绘制网格
ctx.strokeStyle = '#634d31';
ctx.lineWidth = 1;
for (let i = 0; i < GRID_SIZE; i++) {
ctx.beginPath();
ctx.moveTo(cellSize, cellSize * (i + 1));
ctx.lineTo(cellSize * GRID_SIZE, cellSize * (i + 1));
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cellSize * (i + 1), cellSize);
ctx.lineTo(cellSize * (i + 1), cellSize * GRID_SIZE);
ctx.stroke();
}
// 2. 绘制星位
const stars = [[3, 3], [11, 3], [7, 7], [3, 11], [11, 11]];
ctx.fillStyle = '#634d31';
stars.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(cellSize * (x + 1), cellSize * (y + 1), 4, 0, Math.PI * 2);
ctx.fill();
});
// 3. 稳固绘制所有棋子
for (let x = 0; x < GRID_SIZE; x++) {
for (let y = 0; y < GRID_SIZE; y++) {
if (board[x][y] !== 0) {
drawPiece(x, y, board[x][y]);
}
}
}
// 4. 叠加最新的浮动光环
if (lastMove) {
drawLastMoveHalo(lastMove.x, lastMove.y);
}
}
function drawPiece(x, y, type) {
const cx = cellSize * (x + 1);
const cy = cellSize * (y + 1);
const radius = cellSize * 0.43;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
const gradient = ctx.createRadialGradient(cx - radius*0.15, cy - radius*0.15, radius * 0.1, cx, cy, radius);
if (type === 1) {
gradient.addColorStop(0, '#666');
gradient.addColorStop(1, '#000');
} else {
gradient.addColorStop(0, '#fff');
gradient.addColorStop(0.8, '#ddd');
gradient.addColorStop(1, '#bbb');
}
ctx.fillStyle = gradient;
ctx.shadowBlur = 4;
ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 2;
ctx.fill();
ctx.restore();
}
function drawLastMoveHalo(x, y) {
const cx = cellSize * (x + 1);
const cy = cellSize * (y + 1);
const baseRadius = cellSize * 0.43;
const pulse = Math.sin(haloAngle) * 3;
const haloRadius = baseRadius + 3 + pulse;
const opacity = 0.5 - (pulse + 3) * 0.04;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, haloRadius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(74, 144, 226, ${Math.max(0.1, opacity)})`;
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
function animate() {
haloAngle += 0.07;
render();
requestAnimationFrame(animate);
}
canvas.addEventListener('click', function(e) {
if (gameOver || isAiTurn) return;
const rect = canvas.getBoundingClientRect();
const clientX = e.clientX - rect.left;
const clientY = e.clientY - rect.top;
const x = Math.round(clientX / cellSize) - 1;
const y = Math.round(clientY / cellSize) - 1;
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE || board[x][y] !== 0) return;
board[x][y] = 1;
lastMove = { x, y };
render();
if (checkWin(x, y, 1)) {
statusText.innerText = "恭喜,你赢了!🎉";
statusText.style.color = "#2ecc71";
gameOver = true;
return;
}
isAiTurn = true;
statusText.innerText = "AI 正在思考...";
statusText.style.color = "#e67e22";
setTimeout(aiMove, 300);
});
function aiMove() {
if (gameOver) return;
let bestScore = -1;
let bestPoints = [];
for (let x = 0; x < GRID_SIZE; x++) {
for (let y = 0; y < GRID_SIZE; y++) {
if (board[x][y] === 0) {
let aiScore = evaluatePoint(x, y, 2);
let playerScore = evaluatePoint(x, y, 1);
let totalScore = aiScore + playerScore * 0.9;
if (totalScore > bestScore) {
bestScore = totalScore;
bestPoints = [{x, y}];
} else if (totalScore === bestScore) {
bestPoints.push({x, y});
}
}
}
}
if (bestPoints.length === 0) {
statusText.innerText = "平局!";
gameOver = true;
return;
}
const move = bestPoints[Math.floor(Math.random() * bestPoints.length)];
board[move.x][move.y] = 2;
lastMove = { x: move.x, y: move.y };
render();
if (checkWin(move.x, move.y, 2)) {
statusText.innerText = "AI 赢了,再接再厉!";
statusText.style.color = "#e74c3c";
gameOver = true;
return;
}
isAiTurn = false;
statusText.innerText = "你是黑棋,请落子";
statusText.style.color = "#4a90e2";
}
function evaluatePoint(x, y, type) {
let score = 0;
const directions = [[1,0], [0,1], [1,1], [1,-1]];
directions.forEach(([dx, dy]) => {
let count = 1;
let block1 = false;
let block2 = false;
let tx = x + dx, ty = y + dy;
while(tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE) {
if (board[tx][ty] === type) {
count++;
} else {
if (board[tx][ty] !== 0) block1 = true;
break;
}
tx += dx; ty += dy;
}
if (tx < 0 || tx >= GRID_SIZE || ty < 0 || ty >= GRID_SIZE) block1 = true;
tx = x - dx; ty = y - dy;
while(tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE) {
if (board[tx][ty] === type) {
count++;
} else {
if (board[tx][ty] !== 0) block2 = true;
break;
}
tx -= dx; ty -= dy;
}
if (tx < 0 || tx >= GRID_SIZE || ty < 0 || ty >= GRID_SIZE) block2 = true;
if (count >= 5) score += 100000;
else if (count === 4) {
if (!block1 && !block2) score += 10000;
else if (!block1 || !block2) score += 1000;
} else if (count === 3) {
if (!block1 && !block2) score += 1000;
else if (!block1 || !block2) score += 100;
} else if (count === 2) {
if (!block1 && !block2) score += 100;
else if (!block1 || !block2) score += 10;
}
});
return score;
}
function checkWin(x, y, type) {
const directions = [[1,0], [0,1], [1,1], [1,-1]];
for (let [dx, dy] of directions) {
let count = 1;
let tx = x + dx, ty = y + dy;
while (tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE && board[tx][ty] === type) {
count++; tx += dx; ty += dy;
}
tx = x - dx; ty = y - dy;
while (tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE && board[tx][ty] === type) {
count++; tx -= dx; ty -= dy;
}
if (count >= 5) return true;
}
return false;
}
window.addEventListener('resize', initCanvas);
window.onload = () => {
// 先初始化画布(计算 cellSize 等)
initCanvas();
// 再重置游戏(初始化 board 并首次渲染)
restartGame();
// 启动动画循环
animate();
};
</script>
</body>
</html>
改编自 @518 佬发的源码,想玩的佬友也可以试试哈
4 个帖子 - 4 位参与者