<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋 - AI对战</title>
<link rel="stylesheet" href="css/gomoku.css">
</head>
<body>
<div class="game-container">
<h1>五子棋游戏</h1>
<div class="game-info">
<div class="status">轮到你下棋 (黑子)</div>
<button id="restart-btn">重新开始</button>
</div>
<div class="board-container">
<canvas id="game-board" width="600" height="600"></canvas>
</div>
<div class="difficulty">
<label>难度: </label>
<select id="difficulty-select">
<option value="1">简单</option>
<option value="2" selected>中等</option>
<option value="3">困难</option>
</select>
</div>
</div>
<script src="js/gomoku_optimized.js"></script>
</body>
</html>
.game-container {
max-width: 650px;
margin: 20px auto;
padding: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f5f0;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
h1 {
text-align: center;
color: #8B4513;
font-family: 'SimSun', serif;
font-size: 2.2em;
margin-bottom: 25px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
letter-spacing: 2px;
}
.game-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.status {
font-size: 1.2em;
font-weight: bold;
color: #555;
}
#restart-btn {
padding: 10px 20px;
background-color: #8B4513;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
#restart-btn:hover {
background-color: #A0522D;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.25);
}
.board-container {
border: 15px solid #8B4513;
border-radius: 10px;
background: linear-gradient(45deg, #DEB887 0%, #D2B48C 50%, #DEB887 100%);
box-shadow: 0 8px 30px rgba(0,0,0,0.3), inset 0 0 30px rgba(139, 69, 19, 0.2);
padding: 10px;
background-image: url('data:image/svg+xml;utf8,<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/></filter><rect width="200" height="200" filter="url(%23noise)" opacity="0.05"/></svg>'), url('data:image/svg+xml;utf8,<svg width="100" height="200" viewBox="0 0 100 200" xmlns="http://www.w3.org/2000/svg"><path d="M0,0 Q50,50 100,0 Q150,50 100,100 Q50,150 100,200 Q150,150 100,100 Q50,50 0,100 Q-50,150 0,200 Q50,150 0,100 Q-50,50 0,0" fill="none" stroke="rgba(139, 69, 19, 0.1)" stroke-width="2"/></svg>');
background-size: 200px 200px, 100px 200px;
}
#game-board {
display: block;
margin: 0 auto;
background-color: transparent;
cursor: pointer;
border-radius: 5px;
box-shadow: inset 0 0 20px rgba(0,0,0,0.15);
transition: all 0.3s ease;
}
div.difficulty {
margin-top: 15px;
text-align: center;
}
#difficulty-select {
padding: 8px 12px;
font-size: 1em;
margin-left: 10px;
border: 2px solid #8B4513;
border-radius: 6px;
background-color: white;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#difficulty-select:hover {
border-color: #A0522D;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
class GomokuGame {
static get CONSTANTS() {
return {
BOARD_SIZE: 15,
MIN_SIZE: 300,
STONE_RADIUS_RATIO: 0.45,
STAR_POINTS: [3, 7, 11],
ANIMATION_DURATION: 200,
AI_DELAY: 500,
SCORES: {
FIVE_IN_ROW: 100000,
OPEN_FOUR: 10000,
冲四: 1000,
OPEN_THREE: 1000,
冲三: 100,
OPEN_TWO: 100,
冲二: 10,
OPEN_ONE: 10
}
};
}
constructor() {
this.canvas = document.getElementById('game-board');
this.ctx = this.canvas.getContext('2d');
this.restartBtn = document.getElementById('restart-btn');
this.statusElement = document.querySelector('.status');
this.difficultySelect = document.getElementById('difficulty-select');
this.initGameDimensions();
this.board = this.createEmptyBoard();
this.currentPlayer = 1;
this.gameActive = true;
this.difficulty = parseInt(this.difficultySelect.value);
this.animating = false;
this.animationRadius = 0;
this.lastMove = null;
this.hoverPosition = null;
this.winningLine = null;
this.initEventListeners();
this.drawBoard();
this.statusElement.textContent = '轮到你下棋 (黑子)';
}
initGameDimensions() {
const { BOARD_SIZE, MIN_SIZE } = GomokuGame.CONSTANTS;
const container = this.canvas.parentElement;
const containerSize = Math.min(container.clientWidth, container.clientHeight);
const canvasSize = Math.max(containerSize, MIN_SIZE);
this.canvas.width = canvasSize;
this.canvas.height = canvasSize;
this.canvas.style.width = '100%';
this.canvas.style.height = 'auto';
this.cellSize = canvasSize / BOARD_SIZE;
this.stoneRadius = this.cellSize * GomokuGame.CONSTANTS.STONE_RADIUS_RATIO;
}
createEmptyBoard() {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
return Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
}
initEventListeners() {
this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.canvas.addEventListener('mouseleave', () => this.handleMouseLeave());
this.restartBtn.addEventListener('click', () => this.restartGame());
this.difficultySelect.addEventListener('change', (e) => {
this.difficulty = parseInt(e.target.value);
});
window.addEventListener('resize', () => this.handleWindowResize());
}
handleWindowResize() {
this.initGameDimensions();
this.drawBoard();
}
drawBoard() {
this.clearCanvas();
this.drawGrid();
this.drawStarPoints();
this.drawStones();
this.drawLastMoveHighlight();
this.drawWinningLine();
this.drawHoverPreview();
}
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
drawGrid() {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
this.ctx.strokeStyle = '#000';
this.ctx.lineWidth = 1;
for (let i = 0; i < BOARD_SIZE; i++) {
this.ctx.beginPath();
this.ctx.moveTo(this.cellSize / 2, this.cellSize / 2 + i * this.cellSize);
this.ctx.lineTo(this.canvas.width - this.cellSize / 2, this.cellSize / 2 + i * this.cellSize);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(this.cellSize / 2 + i * this.cellSize, this.cellSize / 2);
this.ctx.lineTo(this.cellSize / 2 + i * this.cellSize, this.canvas.height - this.cellSize / 2);
this.ctx.stroke();
}
}
drawStarPoints() {
const { STAR_POINTS } = GomokuGame.CONSTANTS;
this.ctx.fillStyle = '#000';
STAR_POINTS.forEach(x => {
STAR_POINTS.forEach(y => {
this.ctx.beginPath();
this.ctx.arc(
this.cellSize / 2 + x * this.cellSize,
this.cellSize / 2 + y * this.cellSize,
3,
0,
Math.PI * 2
);
this.ctx.fill();
});
});
}
drawStones() {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (this.board[i][j] !== 0) {
this.drawStone(i, j, this.board[i][j]);
}
}
}
}
drawStone(x, y, player, animate = false) {
const centerX = this.cellSize / 2 + x * this.cellSize;
const centerY = this.cellSize / 2 + y * this.cellSize;
const radius = Math.max(animate ? this.animationRadius : this.stoneRadius, 0.1);
const gradient = this.ctx.createRadialGradient(
centerX - 5, centerY - 5, 2,
centerX, centerY, radius
);
if (player === 1) {
gradient.addColorStop(0, '#666');
gradient.addColorStop(1, '#000');
} else {
gradient.addColorStop(0, '#ddd');
gradient.addColorStop(1, '#999');
}
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
this.ctx.strokeStyle = '#000';
this.ctx.lineWidth = 1;
this.ctx.stroke();
}
animateStonePlacement(x, y, player) {
this.animating = true;
this.animationRadius = 0;
const targetRadius = this.stoneRadius;
const duration = GomokuGame.CONSTANTS.ANIMATION_DURATION;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuad = progress * (2 - progress);
this.animationRadius = targetRadius * easeOutQuad;
this.drawBoard();
this.drawStone(x, y, player, true);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.animating = false;
this.animationRadius = targetRadius;
this.drawBoard();
}
};
requestAnimationFrame(animate);
}
handleCanvasClick(e) {
if (!this.gameActive || this.currentPlayer !== 1 || this.animating) return;
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const gridX = Math.round((x - this.cellSize / 2) / this.cellSize);
const gridY = Math.round((y - this.cellSize / 2) / this.cellSize);
if (this.isValidMove(gridX, gridY)) {
this.makeMove(gridX, gridY, this.currentPlayer);
this.animateStonePlacement(gridX, gridY, this.currentPlayer);
if (this.checkGameStatus()) return;
this.currentPlayer = 2;
this.statusElement.textContent = 'AI思考中...';
setTimeout(() => {
this.aiMakeMove();
if (!this.checkGameStatus()) {
this.currentPlayer = 1;
this.statusElement.textContent = '轮到你下棋 (黑子)';
}
}, GomokuGame.CONSTANTS.AI_DELAY);
}
}
isValidMove(x, y) {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && this.board[x][y] === 0;
}
makeMove(x, y, player) {
this.board[x][y] = player;
this.lastMove = { x, y };
}
checkGameStatus() {
if (this.checkWin(this.currentPlayer)) {
this.gameActive = false;
const winner = this.currentPlayer === 1 ? '你赢了!' : 'AI赢了!';
this.statusElement.textContent = winner;
return true;
}
if (this.checkDraw()) {
this.gameActive = false;
this.statusElement.textContent = '平局!';
return true;
}
return false;
}
checkWin(player) {
const directions = [
[1, 0],
[0, 1],
[1, 1],
[1, -1]
];
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (this.board[i][j] === player) {
for (const [dx, dy] of directions) {
const winningLine = this.checkDirection(i, j, dx, dy, player);
if (winningLine.length >= 5) {
this.winningLine = winningLine;
return true;
}
}
}
}
}
this.winningLine = null;
return false;
}
checkDirection(x, y, dx, dy, player) {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
const line = [{ x, y }];
for (let k = 1; k < 5; k++) {
const nx = x + dx * k;
const ny = y + dy * k;
if (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && this.board[nx][ny] === player) {
line.push({ x: nx, y: ny });
} else {
break;
}
}
for (let k = 1; k < 5; k++) {
const nx = x - dx * k;
const ny = y - dy * k;
if (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && this.board[nx][ny] === player) {
line.unshift({ x: nx, y: ny });
} else {
break;
}
}
return line;
}
checkDraw() {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (this.board[i][j] === 0) {
return false;
}
}
}
return true;
}
aiMakeMove() {
const depth = this.getSearchDepth();
const move = this.minimax(depth, -Infinity, Infinity, true);
if (move && this.isValidMove(move.x, move.y)) {
this.makeMove(move.x, move.y, 2);
this.animateStonePlacement(move.x, move.y, 2);
this.checkGameStatus();
} else if (this.checkDraw()) {
this.gameActive = false;
this.statusElement.textContent = '平局!';
}
}
getSearchDepth() {
switch (this.difficulty) {
case 1: return 1;
case 2: return 3;
case 3: return 4;
default: return 3;
}
}
minimax(depth, alpha, beta, isMaximizingPlayer) {
if (depth === 0 || this.checkWin(1) || this.checkWin(2) || this.checkDraw()) {
return { score: this.evaluateBoard() };
}
const originalBoard = JSON.parse(JSON.stringify(this.board));
const moves = this.getAvailableMoves();
if (isMaximizingPlayer) {
return this.maximizeScore(depth, alpha, beta, moves, originalBoard);
} else {
return this.minimizeScore(depth, alpha, beta, moves, originalBoard);
}
}
maximizeScore(depth, alpha, beta, moves, originalBoard) {
let bestScore = -Infinity;
let bestMove = null;
for (const move of moves) {
this.board[move.x][move.y] = 2;
const result = this.minimax(depth - 1, alpha, beta, false);
this.board = JSON.parse(JSON.stringify(originalBoard));
if (result.score > bestScore) {
bestScore = result.score;
bestMove = move;
}
alpha = Math.max(alpha, bestScore);
if (beta <= alpha) break;
}
return bestMove ? { ...bestMove, score: bestScore } : { score: bestScore };
}
minimizeScore(depth, alpha, beta, moves, originalBoard) {
let bestScore = Infinity;
for (const move of moves) {
this.board[move.x][move.y] = 1;
const result = this.minimax(depth - 1, alpha, beta, true);
this.board[move.x][move.y] = 0;
bestScore = Math.min(bestScore, result.score);
beta = Math.min(beta, bestScore);
if (beta <= alpha) break;
}
return { score: bestScore };
}
getAvailableMoves() {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
const moves = [];
const totalStones = this.board.flat().filter(cell => cell !== 0).length;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (this.board[i][j] === 0 && (
totalStones < 5 ||
this.hasAdjacentStones(i, j) ||
(i > 3 && i < 11 && j > 3 && j < 11)
)) {
moves.push({ x: i, y: j, score: this.evaluateMove(i, j) });
}
}
}
if (moves.length === 0) {
return [{ x: Math.floor(BOARD_SIZE / 2), y: Math.floor(BOARD_SIZE / 2) }];
}
return moves.sort((a, b) => b.score - a.score);
}
hasAdjacentStones(x, y) {
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue;
const nx = x + dx;
const ny = y + dy;
if (this.isOnBoard(nx, ny) && this.board[nx][ny] !== 0) {
return true;
}
}
}
return false;
}
isOnBoard(x, y) {
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE;
}
evaluateMove(x, y) {
this.board[x][y] = 2;
const aiScore = this.evaluateLine(x, y, 2);
this.board[x][y] = 1;
const playerScore = this.evaluateLine(x, y, 1);
this.board[x][y] = 0;
return aiScore * 1.2 + playerScore;
}
evaluateBoard() {
let aiScore = 0;
let playerScore = 0;
const { BOARD_SIZE } = GomokuGame.CONSTANTS;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (this.board[i][j] === 2) {
aiScore += this.evaluateLine(i, j, 2);
} else if (this.board[i][j] === 1) {
playerScore += this.evaluateLine(i, j, 1);
}
}
}
return aiScore - playerScore;
}
evaluateLine(x, y, player) {
const directions = [
[1, 0],
[0, 1],
[1, 1],
[1, -1]
];
let totalScore = 0;
for (const [dx, dy] of directions) {
const { count, blocked } = this.countInDirection(x, y, dx, dy, player);
totalScore += this.calculateScore(count, blocked);
}
return totalScore;
}
countInDirection(x, y, dx, dy, player) {
let count = 1;
let blocked = 0;
for (let k = 1; k < 5; k++) {
const nx = x + dx * k;
const ny = y + dy * k;
const result = this.checkPositionStatus(nx, ny, player);
if (result === 'same') count++;
else if (result === 'blocked') { blocked++; break; }
else break;
}
for (let k = 1; k < 5; k++) {
const nx = x - dx * k;
const ny = y - dy * k;
const result = this.checkPositionStatus(nx, ny, player);
if (result === 'same') count++;
else if (result === 'blocked') { blocked++; break; }
else break;
}
return { count, blocked };
}
checkPositionStatus(x, y, player) {
if (!this.isOnBoard(x, y)) return 'blocked';
if (this.board[x][y] === player) return 'same';
if (this.board[x][y] === 0) return 'empty';
return 'blocked';
}
calculateScore(count, blocked) {
const { SCORES } = GomokuGame.CONSTANTS;
if (count >= 5) return SCORES.FIVE_IN_ROW;
if (count === 4) return blocked === 0 ? SCORES.OPEN_FOUR : SCORES.冲四;
if (count === 3) return blocked === 0 ? SCORES.OPEN_THREE : SCORES.冲三;
if (count === 2) return blocked === 0 ? SCORES.OPEN_TWO : SCORES.冲二;
if (count === 1) return blocked === 0 ? SCORES.OPEN_ONE : 0;
return 0;
}
handleMouseMove(e) {
if (!this.gameActive || this.currentPlayer !== 1 || this.animating) {
this.hoverPosition = null;
this.drawBoard();
return;
}
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const gridX = Math.round((x - this.cellSize / 2) / this.cellSize);
const gridY = Math.round((y - this.cellSize / 2) / this.cellSize);
this.hoverPosition = this.isValidMove(gridX, gridY) ? { x: gridX, y: gridY } : null;
this.drawBoard();
}
handleMouseLeave() {
this.hoverPosition = null;
this.drawBoard();
}
drawHoverPreview() {
if (!this.hoverPosition) return;
const { x, y } = this.hoverPosition;
const centerX = this.cellSize / 2 + x * this.cellSize;
const centerY = this.cellSize / 2 + y * this.cellSize;
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, this.stoneRadius, 0, Math.PI * 2);
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
this.ctx.fill();
}
drawLastMoveHighlight() {
if (!this.lastMove) return;
const { x, y } = this.lastMove;
const centerX = this.cellSize / 2 + x * this.cellSize;
const centerY = this.cellSize / 2 + y * this.cellSize;
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, this.stoneRadius + 4, 0, Math.PI * 2);
this.ctx.strokeStyle = '#FFD700';
this.ctx.lineWidth = 3;
this.ctx.stroke();
}
drawWinningLine() {
if (!this.winningLine || this.winningLine.length < 5) return;
this.ctx.beginPath();
const first = this.winningLine[0];
const startX = this.cellSize / 2 + first.x * this.cellSize;
const startY = this.cellSize / 2 + first.y * this.cellSize;
this.ctx.moveTo(startX, startY);
this.winningLine.forEach(point => {
const x = this.cellSize / 2 + point.x * this.cellSize;
const y = this.cellSize / 2 + point.y * this.cellSize;
this.ctx.lineTo(x, y);
});
this.ctx.strokeStyle = '#FF3333';
this.ctx.lineWidth = 5;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.stroke();
}
restartGame() {
this.board = this.createEmptyBoard();
this.currentPlayer = 1;
this.gameActive = true;
this.lastMove = null;
this.hoverPosition = null;
this.winningLine = null;
this.statusElement.textContent = '轮到你下棋 (黑子)';
this.drawBoard();
}
}
window.addEventListener('load', function () {
const game = new GomokuGame();
});