[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>井字棋·究极AI | 无抖动版</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
/* 防止滚动条抖动:固定html/body基础样式 */
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
body {
background: linear-gradient(145deg, #0b1a24 0%, #07121c 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Poppins', 'Roboto', system-ui, -apple-system, sans-serif;
padding: 20px;
}
.game-container {
background: rgba(18, 30, 38, 0.8);
backdrop-filter: blur(3px);
border-radius: 64px;
padding: 24px 28px 36px 28px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 215, 150, 0.25);
transition: all 0.2s;
/* 固定最小高度,避免因内容变化引起页面总高度波动 */
min-height: 620px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.game-header {
text-align: center;
margin-bottom: 24px;
}
h1 {
margin: 0 0 6px 0;
font-size: 2.2rem;
font-weight: 800;
background: linear-gradient(135deg, #FFC857, #FF9430, #E67E22);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: 1px;
}
.sub {
color: #c0e0ff;
font-size: 0.8rem;
font-weight: 500;
background: #00000040;
display: inline-block;
padding: 2px 14px;
border-radius: 40px;
}
.mode-bar {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 28px;
flex-wrap: wrap;
}
.mode-btn {
background: #2c3f44;
border: none;
padding: 8px 28px;
font-size: 1rem;
font-weight: 600;
border-radius: 60px;
cursor: pointer;
color: #eef4ff;
transition: all 0.2s;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
font-family: inherit;
}
.mode-btn.active {
background: linear-gradient(105deg, #F39C12, #E67E22);
color: white;
box-shadow: 0 0 14px rgba(243,156,18,0.7);
border: 1px solid rgba(255,215,0,0.6);
}
.mode-btn:active {
transform: scale(0.96);
}
/* 状态面板固定高度,防止文字换行导致页面高度变化 */
.status-panel {
background: #07141ecc;
border-radius: 80px;
padding: 12px 20px;
margin-bottom: 28px;
text-align: center;
backdrop-filter: blur(8px);
border: 1px solid rgba(255,200,120,0.5);
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.status-text {
font-size: 1.35rem;
font-weight: 700;
color: #FFF2DF;
text-shadow: 0 1px 2px black;
line-height: 1.3;
}
.turn-indicator {
display: inline-block;
background: #00000066;
padding: 4px 14px;
border-radius: 40px;
margin-left: 12px;
font-weight: bold;
font-size: 1.2rem;
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
background-color: #10262e;
padding: 20px;
border-radius: 56px;
box-shadow: inset 0 0 6px #00000044, 0 20px 30px -8px black;
margin-bottom: 28px;
}
.cell {
aspect-ratio: 1 / 1;
background: #fef5e6;
background-image: radial-gradient(circle at 40% 30%, rgba(255,235,200,0.7) 1.8%, transparent 2.2%);
background-size: 20px 20px;
border-radius: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 4.5rem;
font-weight: 800;
font-family: 'Segoe UI', 'Quicksand', system-ui;
cursor: pointer;
transition: 0.12s linear;
box-shadow: 0 10px 0 rgba(0, 0, 0, 0.2);
color: #2c3e4e;
}
.cell.X {
color: #E67E22;
text-shadow: 0 0 6px #FFB347;
}
.cell.O {
color: #2c9b77;
text-shadow: 0 0 6px #4ce0b0;
}
.cell:hover:not(.disabled-cell) {
transform: scale(1.02);
background: #fff8ef;
box-shadow: 0 6px 0 rgba(0, 0, 0, 0.2);
}
.disabled-cell {
cursor: default;
filter: brightness(0.96);
transform: none;
}
.reset-btn {
background: #ffb347;
border: none;
width: 70%;
padding: 14px 0;
font-size: 1.3rem;
font-weight: bold;
border-radius: 60px;
color: #1e2a2f;
display: block;
margin: 0 auto;
cursor: pointer;
transition: all 0.1s;
font-family: inherit;
box-shadow: 0 8px 0 #b4621a;
}
.reset-btn:active {
transform: translateY(3px);
box-shadow: 0 4px 0 #b4621a;
}
.footer-note {
text-align: center;
margin-top: 20px;
font-size: 0.7rem;
color: #8da3b0;
}
@media (max-width: 500px) {
.game-container {
padding: 18px 18px 28px 18px;
min-height: 560px;
}
.cell {
font-size: 3rem;
}
.board {
gap: 10px;
padding: 14px;
}
.status-text {
font-size: 1rem;
}
.mode-btn {
padding: 6px 18px;
font-size: 0.85rem;
}
.status-panel {
min-height: 70px;
padding: 10px 16px;
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="game-header">
<h1>⚡ 天元·不败井字棋 ⚡</h1>
<div class="sub">Minimax + 绝对先手防御 | AI 永不失误</div>
</div>
<div class="mode-bar">
<button class="mode-btn" data-mode="twoPlayer">👥 双人对弈</button>
<button class="mode-btn active" data-mode="vsAI">🤖 人机 · 无敌模式</button>
</div>
<div class="status-panel">
<span class="status-text" id="gameStatusText">✨ X 先手 ✨</span>
<span class="turn-indicator" id="turnIcon">🎲</span>
</div>
<div class="board" id="board"></div>
<button class="reset-btn" id="resetGameBtn">🔄 重开棋局</button>
<div class="footer-note">🤖 强化AI:既会立即获胜,也会精准堵杀 | 结局要么AI赢,要么平局</div>
</div>
<script>
// ---------- DOM 元素 ----------
const boardElement = document.getElementById('board');
const gameStatusSpan = document.getElementById('gameStatusText');
const turnIconSpan = document.getElementById('turnIcon');
const resetBtn = document.getElementById('resetGameBtn');
const modeBtns = document.querySelectorAll('.mode-btn');
// ---------- 游戏核心变量 ----------
let board = Array(9).fill(null); // 0-8 索引, null / 'X' / 'O'
let currentPlayer = 'X'; // X 先手
let gameActive = true;
let gameMode = 'vsAI'; // 'twoPlayer' 或 'vsAI'
let winnerInfo = null; // 'X', 'O', 'draw'
// AI 标识
const AI_PIECE = 'O';
const HUMAN_PIECE = 'X';
let isAIPlaying = false; // 防止AI递归或重复触发
// ---------- 辅助函数 ----------
function updateBoardUI() {
const cells = document.querySelectorAll('.cell');
for (let i = 0; i < cells.length; i++) {
const val = board[i];
cells[i].textContent = val === 'X' ? '✖' : (val === 'O' ? '◯' : '');
if (val === 'X') {
cells[i].classList.add('X');
cells[i].classList.remove('O');
} else if (val === 'O') {
cells[i].classList.add('O');
cells[i].classList.remove('X');
} else {
cells[i].classList.remove('X', 'O');
}
// 禁用样式(有棋子或者游戏结束则不可点)
if (val !== null || !gameActive) {
cells[i].classList.add('disabled-cell');
} else {
cells[i].classList.remove('disabled-cell');
}
}
}
// 检查赢家 (返回 'X', 'O' 或 null)
function checkWinner(boardState) {
const winPatterns = [
[0,1,2], [3,4,5], [6,7,8],
[0,3,6], [1,4,7], [2,5,8],
[0,4,8], [2,4,6]
];
for (let pattern of winPatterns) {
const [a,b,c] = pattern;
if (boardState[a] && boardState[a] === boardState[b] && boardState[a] === boardState[c]) {
return boardState[a];
}
}
return null;
}
function isDraw(boardState) {
return boardState.every(cell => cell !== null) && !checkWinner(boardState);
}
// 更新界面文字
function updateGameStatusDisplay() {
if (!gameActive) {
if (winnerInfo === 'X') {
gameStatusSpan.innerHTML = '🏆 胜利者:✖ X 方!🏆';
turnIconSpan.innerHTML = '⚡';
} else if (winnerInfo === 'O') {
gameStatusSpan.innerHTML = '🏆 胜利者:◯ O 方 (AI)!🏆';
turnIconSpan.innerHTML = '🤖';
} else if (winnerInfo === 'draw') {
gameStatusSpan.innerHTML = '🤝 平局! AI 依然不败 🤝';
turnIconSpan.innerHTML = '🌀';
} else {
gameStatusSpan.innerHTML = '⚙️ 终局 ⚙️';
}
return;
}
if (gameMode === 'twoPlayer') {
if (currentPlayer === 'X') {
gameStatusSpan.innerHTML = '🎯 X 的回合 · 执叉先行 🎯';
turnIconSpan.innerHTML = '❌';
} else {
gameStatusSpan.innerHTML = '🎯 O 的回合 · 画圆博弈 🎯';
turnIconSpan.innerHTML = '⭕';
}
} else { // vsAI 模式
if (currentPlayer === HUMAN_PIECE) {
gameStatusSpan.innerHTML = '🧠 你的回合 (X) · 落子破局 🧠';
turnIconSpan.innerHTML = '✖️';
} else {
gameStatusSpan.innerHTML = '⚙️ AI 极致计算中 (O) · 完美博弈 ⚙️';
turnIconSpan.innerHTML = '🤖';
}
}
}
// 刷新游戏结束标志,更新 active 和 winnerInfo
function refreshGameOverState() {
const winner = checkWinner(board);
if (winner) {
gameActive = false;
winnerInfo = winner;
updateGameStatusDisplay();
updateBoardUI();
return true;
}
if (isDraw(board)) {
gameActive = false;
winnerInfo = 'draw';
updateGameStatusDisplay();
updateBoardUI();
return true;
}
gameActive = true;
winnerInfo = null;
updateGameStatusDisplay();
updateBoardUI();
return false;
}
// 核心落子方法 (不做AI触发, 仅执行落子并更新状态)
function placeMove(index, player) {
if (!gameActive) return false;
if (index < 0 || index > 8) return false;
if (board[index] !== null) return false;
if (player !== currentPlayer) return false;
board[index] = player;
updateBoardUI();
const ended = refreshGameOverState();
if (!ended) {
currentPlayer = (currentPlayer === 'X' ? 'O' : 'X');
updateGameStatusDisplay();
} else {
updateBoardUI();
}
return true;
}
// ---------- 增强型AI:立即获胜 + 防守 + minimax完美决策 ----------
// 获取某玩家能否一步获胜的位置 (返回位置索引,若无返回 -1)
function getImmediateWinMove(boardState, player) {
for (let i = 0; i < 9; i++) {
if (boardState[i] === null) {
boardState[i] = player;
if (checkWinner(boardState) === player) {
boardState[i] = null;
return i;
}
boardState[i] = null;
}
}
return -1;
}
// 防守:返回对手即将获胜的位置(需要堵住的位置)
function getBlockingMove(boardState, opponent) {
for (let i = 0; i < 9; i++) {
if (boardState[i] === null) {
boardState[i] = opponent;
if (checkWinner(boardState) === opponent) {
boardState[i] = null;
return i;
}
boardState[i] = null;
}
}
return -1;
}
// ---------- Minimax 核心 (带 alpha-beta,完美评分) ----------
function evaluateBoard(boardState) {
const winner = checkWinner(boardState);
if (winner === AI_PIECE) return 10;
if (winner === HUMAN_PIECE) return -10;
return 0;
}
function minimax(boardState, isMaximizing, alpha = -Infinity, beta = Infinity) {
const score = evaluateBoard(boardState);
if (score === 10 || score === -10) return score;
if (boardState.every(cell => cell !== null)) return 0;
if (isMaximizing) {
// AI 回合 (O)
let best = -Infinity;
for (let i = 0; i < 9; i++) {
if (boardState[i] === null) {
boardState[i] = AI_PIECE;
let value = minimax(boardState, false, alpha, beta);
boardState[i] = null;
best = Math.max(best, value);
alpha = Math.max(alpha, best);
if (beta <= alpha) break;
}
}
return best;
} else {
// 玩家回合 (X) -> 最小化AI得分
let best = Infinity;
for (let i = 0; i < 9; i++) {
if (boardState[i] === null) {
boardState[i] = HUMAN_PIECE;
let value = minimax(boardState, true, alpha, beta);
boardState[i] = null;
best = Math.min(best, value);
beta = Math.min(beta, best);
if (beta <= alpha) break;
}
}
return best;
}
}
// 获取最佳AI落子(先尝试必胜/防守,再调用Minimax确保全局最优)
function getBestAIMove() {
// 1. 优先自己立即获胜
let winMove = getImmediateWinMove(board, AI_PIECE);
if (winMove !== -1) return winMove;
// 2. 防守玩家立即获胜 (堵杀)
let blockMove = getBlockingMove(board, HUMAN_PIECE);
if (blockMove !== -1) return blockMove;
// 3. 没有立即胜负威胁,使用Minimax全局最优
let bestScore = -Infinity;
let bestMove = -1;
const boardCopy = [...board];
for (let i = 0; i < 9; i++) {
if (boardCopy[i] === null) {
boardCopy[i] = AI_PIECE;
let moveScore = minimax(boardCopy, false);
boardCopy[i] = null;
if (moveScore > bestScore) {
bestScore = moveScore;
bestMove = i;
}
if (bestScore === 10) break;
}
}
return bestMove;
}
// AI 执行移动 (带锁,防止并发)
async function aiMove() {
if (isAIPlaying) return;
if (gameMode !== 'vsAI') return;
if (!gameActive) return;
if (currentPlayer !== AI_PIECE) return;
isAIPlaying = true;
// 微小延迟,让UI更自然,同时保证状态稳定
await new Promise(resolve => setTimeout(resolve, 30));
if (!gameActive || gameMode !== 'vsAI' || currentPlayer !== AI_PIECE) {
isAIPlaying = false;
return;
}
const bestIndex = getBestAIMove();
if (bestIndex !== -1 && board[bestIndex] === null) {
placeMove(bestIndex, AI_PIECE);
}
isAIPlaying = false;
// 若AI移动后游戏未结束,且当前玩家变成了AI(理论上不会,但防止极端情况)
if (gameActive && gameMode === 'vsAI' && currentPlayer === AI_PIECE && !isAIPlaying) {
setTimeout(() => {
if (gameActive && gameMode === 'vsAI' && currentPlayer === AI_PIECE && !isAIPlaying) {
aiMove();
}
}, 20);
}
}
function triggerAIMoveIfNeeded() {
if (gameMode !== 'vsAI') return;
if (!gameActive) return;
if (currentPlayer === AI_PIECE && !isAIPlaying) {
aiMove();
}
}
// ---------- 玩家移动逻辑 ----------
function handlePlayerMove(index) {
if (!gameActive) return false;
if (board[index] !== null) return false;
if (isAIPlaying) return false; // AI思考中不能落子
if (gameMode === 'twoPlayer') {
const success = placeMove(index, currentPlayer);
if (success) {
// 双人模式无需额外AI
return true;
}
return false;
}
else if (gameMode === 'vsAI') {
// 只有人类回合才能下
if (currentPlayer !== HUMAN_PIECE) return false;
const success = placeMove(index, HUMAN_PIECE);
if (success) {
if (gameActive && currentPlayer === AI_PIECE) {
triggerAIMoveIfNeeded();
}
return true;
}
return false;
}
return false;
}
// ---------- 重置游戏(保留当前模式)----------
function resetGame() {
isAIPlaying = false;
board.fill(null);
currentPlayer = 'X'; // X 永远先手
gameActive = true;
winnerInfo = null;
updateBoardUI();
refreshGameOverState();
// 人机模式下,重置后玩家先手(X),AI为O后手,不需要立即触发AI
if (gameMode === 'vsAI' && currentPlayer === AI_PIECE) {
// 正常情况下不会发生,但若发生则触发AI
if (gameActive && !isAIPlaying) {
triggerAIMoveIfNeeded();
}
}
}
// 切换模式
function setGameMode(mode) {
gameMode = mode;
resetGame();
// 更新按钮样式
modeBtns.forEach(btn => {
if (btn.getAttribute('data-mode') === mode) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
updateGameStatusDisplay();
}
// ---------- 棋盘UI构建 ----------
function createBoardUI() {
boardElement.innerHTML = '';
for (let i = 0; i < 9; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.setAttribute('data-index', i);
cell.addEventListener('click', (e) => {
const idx = parseInt(cell.getAttribute('data-index'));
handlePlayerMove(idx);
});
boardElement.appendChild(cell);
}
updateBoardUI();
}
// 事件绑定
function initEventListeners() {
resetBtn.addEventListener('click', () => {
resetGame();
});
modeBtns.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.getAttribute('data-mode');
if (mode === 'twoPlayer') setGameMode('twoPlayer');
else if (mode === 'vsAI') setGameMode('vsAI');
});
});
}
// 初始化
function init() {
createBoardUI();
initEventListeners();
setGameMode('vsAI'); // 默认无敌AI模式
refreshGameOverState();
}
init();
</script>
</body>
</html>