gobang / index.html
kevin213's picture
Add 2 files
5ed7ec6 verified
<!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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.board {
display: grid;
grid-template-columns: repeat(15, 1fr);
grid-template-rows: repeat(15, 1fr);
gap: 1px;
background-color: #e5c07b;
border: 2px solid #5c3a21;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
width: 600px;
height: 600px;
}
.cell {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(229, 192, 123, 0.8);
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.cell:hover {
background-color: rgba(229, 192, 123, 0.6);
}
.cell::before, .cell::after {
content: '';
position: absolute;
background-color: #5c3a21;
}
.cell::before {
width: 100%;
height: 1px;
top: 50%;
}
.cell::after {
width: 1px;
height: 100%;
left: 50%;
}
.piece {
width: 36px;
height: 36px;
border-radius: 50%;
z-index: 10;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3);
}
.black {
background: radial-gradient(circle at 30% 30%, #666, #000);
}
.white {
background: radial-gradient(circle at 30% 30%, #fff, #ccc);
border: 1px solid #999;
}
.last-move {
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.7);
}
.win-line {
position: absolute;
background-color: rgba(255, 0, 0, 0.7);
z-index: 5;
transform-origin: left center;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
animation: fadeIn 0.3s ease-out;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div class="max-w-6xl w-full">
<h1 class="text-3xl font-bold text-center mb-6 text-gray-800">五子棋对战</h1>
<div class="flex flex-col md:flex-row gap-6 items-center md:items-start justify-center">
<!-- 游戏面板 -->
<div class="relative">
<div class="board mx-auto">
<!-- 棋盘将通过JavaScript动态生成 -->
</div>
<!-- 游戏状态指示器 -->
<div class="mt-4 flex justify-between items-center px-2">
<div class="flex items-center">
<div class="w-6 h-6 rounded-full bg-black mr-2"></div>
<span id="black-score" class="font-medium">0</span>
</div>
<div id="game-status" class="px-4 py-1 bg-gray-200 rounded-full text-sm font-medium">黑方回合</div>
<div class="flex items-center">
<div class="w-6 h-6 rounded-full bg-white border border-gray-400 mr-2"></div>
<span id="white-score" class="font-medium">0</span>
</div>
</div>
</div>
<!-- 游戏控制面板 -->
<div class="bg-white p-6 rounded-xl shadow-md w-full max-w-md">
<h2 class="text-xl font-semibold mb-4 text-gray-800">游戏设置</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">游戏模式</label>
<div class="flex gap-2">
<button id="pvp-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md flex-1 hover:bg-blue-700 transition">
<i class="fas fa-users mr-2"></i>双人对战
</button>
<button id="ai-btn" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md flex-1 hover:bg-gray-300 transition">
<i class="fas fa-robot mr-2"></i>人机对战
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">AI难度</label>
<select id="ai-level" class="w-full p-2 border border-gray-300 rounded-md" disabled>
<option value="1">简单</option>
<option value="2" selected>中等</option>
<option value="3">困难</option>
</select>
</div>
<div class="pt-2">
<button id="new-game-btn" class="w-full py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition flex items-center justify-center">
<i class="fas fa-plus-circle mr-2"></i>新游戏
</button>
</div>
<div class="pt-2">
<button id="undo-btn" class="w-full py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600 transition flex items-center justify-center">
<i class="fas fa-undo mr-2"></i>悔棋
</button>
</div>
<div class="pt-2">
<button id="restart-btn" class="w-full py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition flex items-center justify-center">
<i class="fas fa-redo mr-2"></i>重新开始
</button>
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-200">
<h3 class="text-sm font-medium text-gray-700 mb-2">游戏记录</h3>
<div id="move-history" class="max-h-40 overflow-y-auto text-sm space-y-1">
<div class="text-gray-500 italic">游戏尚未开始</div>
</div>
</div>
</div>
</div>
</div>
<!-- 胜利模态框 -->
<div id="win-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden modal">
<div class="bg-white p-8 rounded-xl max-w-md w-full mx-4">
<div class="text-center">
<h2 id="win-title" class="text-2xl font-bold mb-4"></h2>
<div id="win-message" class="mb-6"></div>
<div class="flex gap-4 justify-center">
<button id="play-again-btn" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
再玩一局
</button>
<button id="close-modal-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition">
关闭
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 游戏状态
const gameState = {
board: Array(15).fill().map(() => Array(15).fill(0)), // 0=空, 1=黑, 2=白
currentPlayer: 1, // 1=黑, 2=白
gameOver: false,
lastMove: null,
moveHistory: [],
gameMode: 'pvp', // 'pvp' or 'ai'
aiLevel: 2, // 1-3
scores: { black: 0, white: 0 }
};
// DOM元素
const boardElement = document.querySelector('.board');
const gameStatusElement = document.getElementById('game-status');
const blackScoreElement = document.getElementById('black-score');
const whiteScoreElement = document.getElementById('white-score');
const moveHistoryElement = document.getElementById('move-history');
const pvpBtn = document.getElementById('pvp-btn');
const aiBtn = document.getElementById('ai-btn');
const aiLevelSelect = document.getElementById('ai-level');
const newGameBtn = document.getElementById('new-game-btn');
const undoBtn = document.getElementById('undo-btn');
const restartBtn = document.getElementById('restart-btn');
const winModal = document.getElementById('win-modal');
const winTitle = document.getElementById('win-title');
const winMessage = document.getElementById('win-message');
const playAgainBtn = document.getElementById('play-again-btn');
const closeModalBtn = document.getElementById('close-modal-btn');
// 初始化棋盘
function initBoard() {
boardElement.innerHTML = '';
for (let row = 0; row < 15; row++) {
for (let col = 0; col < 15; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = row;
cell.dataset.col = col;
cell.addEventListener('click', () => handleCellClick(row, col));
boardElement.appendChild(cell);
}
}
}
// 处理点击棋盘
function handleCellClick(row, col) {
if (gameState.gameOver || gameState.board[row][col] !== 0) return;
// 人机模式下,如果是AI的回合,不允许玩家下子
if (gameState.gameMode === 'ai' && gameState.currentPlayer === 2) return;
makeMove(row, col);
// 人机模式下,玩家下完后AI下子
if (gameState.gameMode === 'ai' && !gameState.gameOver && gameState.currentPlayer === 2) {
setTimeout(() => {
makeAIMove();
}, 500);
}
}
// 下子
function makeMove(row, col) {
gameState.board[row][col] = gameState.currentPlayer;
gameState.lastMove = { row, col, player: gameState.currentPlayer };
gameState.moveHistory.push({ row, col, player: gameState.currentPlayer });
updateBoard();
// 检查是否胜利
if (checkWin(row, col)) {
gameState.gameOver = true;
const winner = gameState.currentPlayer === 1 ? '黑方' : '白方';
// 更新分数
if (gameState.currentPlayer === 1) {
gameState.scores.black++;
blackScoreElement.textContent = gameState.scores.black;
} else {
gameState.scores.white++;
whiteScoreElement.textContent = gameState.scores.white;
}
showWinModal(`${winner}胜利!`, `恭喜${winner}获得胜利!`);
return;
}
// 切换玩家
gameState.currentPlayer = gameState.currentPlayer === 1 ? 2 : 1;
updateGameStatus();
// 添加到历史记录
addToMoveHistory(row, col);
}
// AI下子
function makeAIMove() {
if (gameState.gameOver) return;
let row, col;
// 简单AI - 随机下子
if (gameState.aiLevel === 1) {
do {
row = Math.floor(Math.random() * 15);
col = Math.floor(Math.random() * 15);
} while (gameState.board[row][col] !== 0);
}
// 中等AI - 简单评估
else if (gameState.aiLevel === 2) {
const move = findBestMove(1); // 只考虑一步
row = move.row;
col = move.col;
}
// 困难AI - 更复杂的评估
else {
const move = findBestMove(2); // 考虑两步
row = move.row;
col = move.col;
}
makeMove(row, col);
}
// 寻找最佳下法 (简化版)
function findBestMove(depth) {
// 优先检查是否有可以立即获胜的位置
for (let row = 0; row < 15; row++) {
for (let col = 0; col < 15; col++) {
if (gameState.board[row][col] === 0) {
// 检查AI是否能在这里获胜
gameState.board[row][col] = 2;
if (checkWin(row, col)) {
gameState.board[row][col] = 0;
return { row, col, score: 10000 };
}
gameState.board[row][col] = 0;
// 检查玩家是否能在这里获胜 (需要阻止)
gameState.board[row][col] = 1;
if (checkWin(row, col)) {
gameState.board[row][col] = 0;
return { row, col, score: 9000 };
}
gameState.board[row][col] = 0;
}
}
}
// 如果没有立即获胜或阻止的位置,则评估棋盘
const moves = [];
// 只在已有棋子周围的空位考虑 (提高效率)
const consideredPositions = new Set();
for (let row = 0; row < 15; row++) {
for (let col = 0; col < 15; col++) {
if (gameState.board[row][col] !== 0) {
// 检查周围3x3区域
for (let r = Math.max(0, row - 1); r <= Math.min(14, row + 1); r++) {
for (let c = Math.max(0, col - 1); c <= Math.min(14, col + 1); c++) {
if (gameState.board[r][c] === 0 && !consideredPositions.has(`${r},${c}`)) {
consideredPositions.add(`${r},${c}`);
const score = evaluatePosition(r, c);
moves.push({ row: r, col: c, score });
}
}
}
}
}
}
// 如果没有可考虑的位置 (开局),随机选择中心区域
if (moves.length === 0) {
const centerMoves = [];
for (let row = 5; row <= 9; row++) {
for (let col = 5; col <= 9; col++) {
if (gameState.board[row][col] === 0) {
centerMoves.push({ row, col, score: 100 });
}
}
}
if (centerMoves.length > 0) {
return centerMoves[Math.floor(Math.random() * centerMoves.length)];
}
// 如果中心区域也没有位置,随机选择
let row, col;
do {
row = Math.floor(Math.random() * 15);
col = Math.floor(Math.random() * 15);
} while (gameState.board[row][col] !== 0);
return { row, col, score: 0 };
}
// 按分数排序并返回最佳移动
moves.sort((a, b) => b.score - a.score);
return moves[0];
}
// 评估位置分数
function evaluatePosition(row, col) {
let score = 0;
// 四个方向: 水平, 垂直, 对角线, 反对角线
const directions = [
[0, 1], // 水平
[1, 0], // 垂直
[1, 1], // 对角线
[1, -1] // 反对角线
];
for (const [dx, dy] of directions) {
// 评估AI (白子) 的潜力
score += evaluateLine(row, col, dx, dy, 2) * 2;
// 评估玩家 (黑子) 的潜力 (需要阻止)
score += evaluateLine(row, col, dx, dy, 1);
}
// 中心位置更有价值
const centerDist = Math.abs(row - 7) + Math.abs(col - 7);
score += (14 - centerDist) * 10;
return score;
}
// 评估一条线上的潜力
function evaluateLine(row, col, dx, dy, player) {
let score = 0;
let count = 0; // 连续的同色棋子
let empty = 0; // 两端的空位
// 向一个方向检查
for (let i = 1; i <= 4; i++) {
const r = row + i * dx;
const c = col + i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15) break;
if (gameState.board[r][c] === player) {
count++;
} else if (gameState.board[r][c] === 0) {
empty++;
break;
} else {
break;
}
}
// 向相反方向检查
for (let i = 1; i <= 4; i++) {
const r = row - i * dx;
const c = col - i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15) break;
if (gameState.board[r][c] === player) {
count++;
} else if (gameState.board[r][c] === 0) {
empty++;
break;
} else {
break;
}
}
// 根据连续棋子和空位计算分数
if (count >= 4) return 10000; // 可以形成五连
if (count === 3 && empty === 2) return 5000; // 活四
if (count === 3 && empty === 1) return 1000; // 冲四
if (count === 2 && empty === 2) return 500; // 活三
if (count === 2 && empty === 1) return 100; // 冲三
if (count === 1 && empty === 2) return 50; // 活二
return 0;
}
// 检查是否胜利
function checkWin(row, col) {
const player = gameState.board[row][col];
const directions = [
[0, 1], // 水平
[1, 0], // 垂直
[1, 1], // 对角线
[1, -1] // 反对角线
];
for (const [dx, dy] of directions) {
let count = 1; // 已经有一个棋子
// 向一个方向检查
for (let i = 1; i <= 4; i++) {
const r = row + i * dx;
const c = col + i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || gameState.board[r][c] !== player) break;
count++;
}
// 向相反方向检查
for (let i = 1; i <= 4; i++) {
const r = row - i * dx;
const c = col - i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || gameState.board[r][c] !== player) break;
count++;
}
if (count >= 5) {
// 绘制胜利线
drawWinLine(row, col, dx, dy, count);
return true;
}
}
return false;
}
// 绘制胜利线
function drawWinLine(row, col, dx, dy, count) {
// 找到线的起点和终点
let startRow = row;
let startCol = col;
let endRow = row;
let endCol = col;
// 向一个方向延伸
for (let i = 1; i <= 4; i++) {
const r = row + i * dx;
const c = col + i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || gameState.board[r][c] !== gameState.board[row][col]) break;
endRow = r;
endCol = c;
}
// 向相反方向延伸
for (let i = 1; i <= 4; i++) {
const r = row - i * dx;
const c = col - i * dy;
if (r < 0 || r >= 15 || c < 0 || c >= 15 || gameState.board[r][c] !== gameState.board[row][col]) break;
startRow = r;
startCol = c;
}
// 创建线元素
const line = document.createElement('div');
line.className = 'win-line';
// 计算线的位置和角度
const cellSize = boardElement.offsetWidth / 15;
const startX = (startCol + 0.5) * cellSize;
const startY = (startRow + 0.5) * cellSize;
const endX = (endCol + 0.5) * cellSize;
const endY = (endRow + 0.5) * cellSize;
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 = '4px';
line.style.left = `${startX}px`;
line.style.top = `${startY}px`;
line.style.transform = `rotate(${angle}deg)`;
boardElement.appendChild(line);
}
// 更新棋盘显示
function updateBoard() {
// 清除所有棋子
document.querySelectorAll('.piece').forEach(p => p.remove());
// 重新绘制所有棋子
for (let row = 0; row < 15; row++) {
for (let col = 0; col < 15; col++) {
if (gameState.board[row][col] !== 0) {
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
const piece = document.createElement('div');
piece.className = `piece ${gameState.board[row][col] === 1 ? 'black' : 'white'}`;
// 标记最后一步
if (gameState.lastMove && gameState.lastMove.row === row && gameState.lastMove.col === col) {
piece.classList.add('last-move');
}
cell.appendChild(piece);
}
}
}
}
// 更新游戏状态显示
function updateGameStatus() {
if (gameState.gameOver) {
gameStatusElement.textContent = '游戏结束';
gameStatusElement.className = 'px-4 py-1 bg-gray-400 rounded-full text-sm font-medium text-white';
return;
}
if (gameState.currentPlayer === 1) {
gameStatusElement.textContent = '黑方回合';
gameStatusElement.className = 'px-4 py-1 bg-gray-800 rounded-full text-sm font-medium text-white';
} else {
gameStatusElement.textContent = gameState.gameMode === 'pvp' ? '白方回合' : 'AI思考中...';
gameStatusElement.className = 'px-4 py-1 bg-white border border-gray-300 rounded-full text-sm font-medium';
}
}
// 添加到历史记录
function addToMoveHistory(row, col) {
const moveNum = gameState.moveHistory.length;
const player = gameState.moveHistory[moveNum - 1].player;
const moveText = `${moveNum}. ${player === 1 ? '黑' : '白'} (${String.fromCharCode(65 + col)}${15 - row})`;
// 如果历史记录为空,清除占位文本
if (moveHistoryElement.firstChild && moveHistoryElement.firstChild.textContent === '游戏尚未开始') {
moveHistoryElement.innerHTML = '';
}
const moveElement = document.createElement('div');
moveElement.className = 'flex justify-between items-center py-1 px-2 bg-gray-100 rounded';
moveElement.innerHTML = `
<span>${moveText}</span>
<button class="text-blue-600 hover:text-blue-800 text-xs" data-move="${moveNum - 1}">
<i class="fas fa-undo"></i>
</button>
`;
moveHistoryElement.prepend(moveElement);
// 为悔棋按钮添加事件
moveElement.querySelector('button').addEventListener('click', (e) => {
const moveIndex = parseInt(e.target.closest('button').dataset.move);
undoToMove(moveIndex);
});
// 限制历史记录数量
if (moveHistoryElement.children.length > 10) {
moveHistoryElement.removeChild(moveHistoryElement.lastChild);
}
}
// 悔棋到指定步数
function undoToMove(moveIndex) {
if (gameState.gameOver) return;
// 重置游戏状态
gameState.board = Array(15).fill().map(() => Array(15).fill(0));
gameState.currentPlayer = 1;
gameState.gameOver = false;
gameState.lastMove = null;
// 重新执行所有步数直到指定步
for (let i = 0; i <= moveIndex; i++) {
const move = gameState.moveHistory[i];
gameState.board[move.row][move.col] = move.player;
gameState.currentPlayer = move.player === 1 ? 2 : 1;
gameState.lastMove = move;
}
// 截断历史记录
gameState.moveHistory = gameState.moveHistory.slice(0, moveIndex + 1);
// 更新显示
updateBoard();
updateGameStatus();
// 重新生成历史记录
moveHistoryElement.innerHTML = '';
for (let i = 0; i <= moveIndex; i++) {
addToMoveHistory(gameState.moveHistory[i].row, gameState.moveHistory[i].col);
}
// 如果是AI模式且是AI的回合,让AI下子
if (gameState.gameMode === 'ai' && gameState.currentPlayer === 2) {
setTimeout(() => {
makeAIMove();
}, 500);
}
}
// 显示胜利模态框
function showWinModal(title, message) {
winTitle.textContent = title;
winMessage.innerHTML = `
<div class="flex justify-center mb-4">
<div class="w-16 h-16 rounded-full ${gameState.currentPlayer === 1 ? 'bg-black' : 'bg-white border border-gray-400'}"></div>
</div>
<p class="text-lg">${message}</p>
`;
winModal.classList.remove('hidden');
}
// 新游戏
function newGame() {
// 重置游戏状态
gameState.board = Array(15).fill().map(() => Array(15).fill(0));
gameState.currentPlayer = 1;
gameState.gameOver = false;
gameState.lastMove = null;
gameState.moveHistory = [];
// 清除胜利线
document.querySelectorAll('.win-line').forEach(line => line.remove());
// 更新显示
updateBoard();
updateGameStatus();
moveHistoryElement.innerHTML = '<div class="text-gray-500 italic">游戏尚未开始</div>';
// 如果是AI模式且AI先手,让AI下子
if (gameState.gameMode === 'ai' && gameState.currentPlayer === 2) {
setTimeout(() => {
makeAIMove();
}, 500);
}
}
// 重新开始 (重置分数)
function restartGame() {
gameState.scores = { black: 0, white: 0 };
blackScoreElement.textContent = '0';
whiteScoreElement.textContent = '0';
newGame();
}
// 事件监听器
pvpBtn.addEventListener('click', () => {
gameState.gameMode = 'pvp';
pvpBtn.className = 'px-4 py-2 bg-blue-600 text-white rounded-md flex-1 hover:bg-blue-700 transition';
aiBtn.className = 'px-4 py-2 bg-gray-200 text-gray-800 rounded-md flex-1 hover:bg-gray-300 transition';
aiLevelSelect.disabled = true;
newGame();
});
aiBtn.addEventListener('click', () => {
gameState.gameMode = 'ai';
pvpBtn.className = 'px-4 py-2 bg-gray-200 text-gray-800 rounded-md flex-1 hover:bg-gray-300 transition';
aiBtn.className = 'px-4 py-2 bg-blue-600 text-white rounded-md flex-1 hover:bg-blue-700 transition';
aiLevelSelect.disabled = false;
newGame();
});
aiLevelSelect.addEventListener('change', () => {
gameState.aiLevel = parseInt(aiLevelSelect.value);
});
newGameBtn.addEventListener('click', newGame);
undoBtn.addEventListener('click', () => {
if (gameState.moveHistory.length > 0) {
undoToMove(gameState.moveHistory.length - 2);
}
});
restartBtn.addEventListener('click', restartGame);
playAgainBtn.addEventListener('click', () => {
winModal.classList.add('hidden');
newGame();
});
closeModalBtn.addEventListener('click', () => {
winModal.classList.add('hidden');
});
// 初始化游戏
initBoard();
updateGameStatus();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=kevin213/gobang" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>