|
|
<!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"> |
|
|
|
|
|
</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)), |
|
|
currentPlayer: 1, |
|
|
gameOver: false, |
|
|
lastMove: null, |
|
|
moveHistory: [], |
|
|
gameMode: 'pvp', |
|
|
aiLevel: 2, |
|
|
scores: { black: 0, white: 0 } |
|
|
}; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
if (gameState.gameMode === 'ai' && gameState.currentPlayer === 2) return; |
|
|
|
|
|
makeMove(row, col); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
function makeAIMove() { |
|
|
if (gameState.gameOver) return; |
|
|
|
|
|
let row, col; |
|
|
|
|
|
|
|
|
if (gameState.aiLevel === 1) { |
|
|
do { |
|
|
row = Math.floor(Math.random() * 15); |
|
|
col = Math.floor(Math.random() * 15); |
|
|
} while (gameState.board[row][col] !== 0); |
|
|
} |
|
|
|
|
|
else if (gameState.aiLevel === 2) { |
|
|
const move = findBestMove(1); |
|
|
row = move.row; |
|
|
col = move.col; |
|
|
} |
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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>'; |
|
|
|
|
|
|
|
|
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> |