<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>🎮 增强版五子棋</title>
<style>
:root {
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body {
background: #f4f4f4;
font-family: -apple-system, 'Microsoft Yahei', sans-serif;
text-align: center;
margin: 0;
padding: 0;
padding-top: 10px;
padding-bottom: calc(80px + var(--safe-bottom));
transition: all 0.3s ease;
touch-action: manipulation; /* 禁止双击缩放 */
overscroll-behavior: none;
}
h1 {
color: #333;
font-size: clamp(16px, 4vw, 24px);
margin: 8px 0;
padding: 0 10px;
line-height: 1.3;
}
/* 顶部控制区 - 移动端紧凑排列 */
.top-controls {
display: flex;
justify-content: center;
gap: 8px;
padding: 5px 10px;
flex-wrap: wrap;
}
.top-controls label {
font-size: 13px;
display: flex;
align-items: center;
gap: 4px;
}
select {
font-size: 13px;
padding: 4px 6px;
border-radius: 6px;
border: 1px solid #999;
max-width: 120px;
appearance: auto;
}
/* Canvas 响应式 */
.canvas-wrapper {
width: 100%;
display: flex;
justify-content: center;
padding: 8px;
}
canvas {
border: 2px solid #333;
background-color: #fdd;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
/* 关键:Canvas大小由JS动态设置,CSS只约束最大值 */
max-width: calc(100vw - 16px);
max-height: calc(100vw - 16px);
touch-action: none; /* 禁止Canvas上的浏览器默认手势 */
}
#status {
font-size: clamp(14px, 3.5vw, 18px);
margin: 6px 0;
color: green;
min-height: 24px;
padding: 0 10px;
}
#score {
font-size: clamp(12px, 3vw, 16px);
margin: 4px 0;
padding: 0 10px;
line-height: 1.5;
}
/* 底部固定操作栏 - 移动端核心UI */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: inherit;
border-top: 1px solid rgba(0,0,0,0.1);
padding: 10px 10px calc(10px + var(--safe-bottom));
display: flex;
justify-content: center;
gap: 8px;
z-index: 100;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
button {
padding: 10px 14px;
font-size: clamp(13px, 3vw, 16px);
cursor: pointer;
border-radius: 8px;
border: 1px solid #999;
background: #fff;
transition: all 0.15s ease;
flex: 1;
max-width: 140px;
white-space: nowrap;
/* 移动端触摸反馈 */
-webkit-user-select: none;
user-select: none;
}
button:active:not(:disabled) {
transform: scale(0.95);
opacity: 0.8;
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.footer-info {
font-size: 11px;
color: #888;
margin-top: 4px;
padding: 0 10px;
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
body { padding-top: 5px; padding-bottom: calc(60px + var(--safe-bottom)); }
h1 { font-size: 14px; margin: 4px 0; }
.top-controls { padding: 2px 10px; }
.canvas-wrapper { padding: 4px; }
canvas {
max-width: calc(100vh - 120px);
max-height: calc(100vh - 120px);
}
.bottom-bar { padding: 6px 10px calc(6px + var(--safe-bottom)); }
button { padding: 8px 10px; }
#status { margin: 3px 0; min-height: 20px; }
#score { display: none; } /* 横屏隐藏战绩节省空间 */
.footer-info { display: none; }
}
/* 超小屏幕适配 */
@media (max-width: 360px) {
button { padding: 8px 8px; font-size: 12px; }
.top-controls { gap: 4px; }
select { max-width: 100px; font-size: 12px; }
}
</style>
</head>
<body>
<h1>🎮 增强版五子棋</h1>
<div class="top-controls">
<label>模式:
<select id="gameMode">
<option value="local">双人对战</option>
<option value="ai">挑战电脑</option>
</select>
</label>
<label>主题:
<select id="themeSelect">
<option value="classic">🟤 经典</option>
<option value="dark">🌙 暗夜</option>
<option value="modern">💎 现代蓝</option>
<option value="nature">🌿 自然绿</option>
<option value="sunset">🌅 夕阳橙</option>
<option value="purple">🌸 梦幻紫</option>
</select>
</label>
</div>
<div class="canvas-wrapper">
<canvas id="chessBoard"></canvas>
</div>
<div id="status">轮到黑棋落子</div>
<div id="score">🎮 你(黑):0胜 电脑(白):0胜</div>
<div class="footer-info">💡 AI模式撤销会同时撤回电脑棋步</div>
<!-- 底部固定操作栏 -->
<div class="bottom-bar">
<button id="undoBtn" onclick="undoMove()" disabled>⬅️ 撤销</button>
<button onclick="restartGame()">🔄 重来</button>
<button onclick="resetScores()">🗑️ 清战绩</button>
</div>
<audio id="placeSound">
<source src="https://cdn.pixabay.com/audio/2022/03/15/audio_4b7f9a1ba6.mp3" type="audio/mp3">
</audio>
<audio id="winSound">
<source src="https://cdn.pixabay.com/audio/2022/01/17/audio_c46dc4b0ac.mp3" type="audio/mp3">
</audio>
<script>
// ========== 主题定义 ==========
const themes = {
classic: {
background: "#f4f4f4", canvasBg: "#fdd", canvasBorder: "#333",
boardLines: "#333", textColor: "#333", statusColor: "green",
blackGradient: ["#333", "#000"], whiteGradient: ["#fff", "#ddd"],
whiteBorder: "#999", starColor: "#333",
buttonBg: "#fff", buttonBorder: "#999",
shadow: "0 0 15px rgba(0,0,0,0.2)", barBorder: "rgba(0,0,0,0.1)"
},
dark: {
background: "#1a1a2e", canvasBg: "#2d2d44", canvasBorder: "#4a4a6a",
boardLines: "#7a7a9a", textColor: "#e0e0e0", statusColor: "#4ade80",
blackGradient: ["#222", "#000"], whiteGradient: ["#f0f0f0", "#c0c0c0"],
whiteBorder: "#888", starColor: "#7a7a9a",
buttonBg: "#2d2d44", buttonBorder: "#4a4a6a",
shadow: "0 0 20px rgba(100,100,255,0.3)", barBorder: "rgba(255,255,255,0.1)"
},
modern: {
background: "#e8f4f8", canvasBg: "#b8d8e8", canvasBorder: "#2c3e50",
boardLines: "#2c3e50", textColor: "#2c3e50", statusColor: "#2980b9",
blackGradient: ["#34495e", "#1a252f"], whiteGradient: ["#fff", "#bdc3c7"],
whiteBorder: "#95a5a6", starColor: "#2c3e50",
buttonBg: "#ecf0f1", buttonBorder: "#bdc3c7",
shadow: "0 0 15px rgba(44,62,80,0.2)", barBorder: "rgba(44,62,80,0.1)"
},
nature: {
background: "#e8f5e9", canvasBg: "#c8e6c9", canvasBorder: "#1b5e20",
boardLines: "#2e7d32", textColor: "#1b5e20", statusColor: "#2e7d32",
blackGradient: ["#333", "#111"], whiteGradient: ["#fff", "#ddd"],
whiteBorder: "#757575", starColor: "#1b5e20",
buttonBg: "#e8f5e9", buttonBorder: "#81c784",
shadow: "0 0 15px rgba(27,94,32,0.2)", barBorder: "rgba(27,94,32,0.1)"
},
sunset: {
background: "#fff3e0", canvasBg: "#ffe0b2", canvasBorder: "#e65100",
boardLines: "#bf360c", textColor: "#bf360c", statusColor: "#e65100",
blackGradient: ["#4e342e", "#1b0000"], whiteGradient: ["#fff", "#ffe082"],
whiteBorder: "#f9a825", starColor: "#bf360c",
buttonBg: "#fff3e0", buttonBorder: "#ff8f00",
shadow: "0 0 15px rgba(230,81,0,0.2)", barBorder: "rgba(230,81,0,0.1)"
},
purple: {
background: "#f3e5f5", canvasBg: "#e1bee7", canvasBorder: "#4a148c",
boardLines: "#6a1b9a", textColor: "#4a148c", statusColor: "#7b1fa2",
blackGradient: ["#2d0052", "#0a0015"], whiteGradient: ["#fff", "#f8bbd0"],
whiteBorder: "#ad1457", starColor: "#6a1b9a",
buttonBg: "#f3e5f5", buttonBorder: "#ab47bc",
shadow: "0 0 15px rgba(106,27,154,0.2)", barBorder: "rgba(106,27,154,0.1)"
}
};
// ========== 全局变量 ==========
const canvas = document.getElementById("chessBoard");
const ctx = canvas.getContext("2d");
const statusDiv = document.getElementById("status");
const scoreDiv = document.getElementById("score");
const undoBtn = document.getElementById("undoBtn");
const boardSize = 15;
let cellSize = 0; // 动态计算
let chessBoard = [];
let gameOver = false;
let currentPlayer = "black";
let gameMode = "local";
let moveHistory = [];
let currentTheme = themes.classic;
let aiThinking = false;
let aiTimeout = null;
// ========== 响应式Canvas尺寸 ==========
function resizeCanvas() {
const wrapper = document.querySelector('.canvas-wrapper');
const maxSize = Math.min(wrapper.clientWidth - 16, window.innerHeight * 0.65);
// 确保是整数避免模糊
const size = Math.floor(Math.max(280, maxSize));
// 处理高清屏
const dpr = window.devicePixelRatio || 1;
canvas.width = size * dpr;
canvas.height = size * dpr;
canvas.style.width = size + 'px';
canvas.style.height = size + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cellSize = size / (boardSize + 1);
drawBoard();
}
// 监听窗口变化,防抖处理
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resizeCanvas, 100);
});
// ========== 初始化 ==========
function initScores() {
if (!localStorage.getItem("blackWins")) localStorage.setItem("blackWins", "0");
if (!localStorage.getItem("whiteWins")) localStorage.setItem("whiteWins", "0");
updateScoreDisplay();
}
function resetScores() {
if (confirm("确定清除所有战绩?")) {
localStorage.setItem("blackWins", "0");
localStorage.setItem("whiteWins", "0");
updateScoreDisplay();
}
}
function updateScoreDisplay() {
const b = localStorage.getItem("blackWins");
const w = localStorage.getItem("whiteWins");
scoreDiv.innerHTML = `🎮 你(黑):${b}胜 电脑(白):${w}胜`;
}
function playPlaceSound() {
const s = document.getElementById("placeSound");
s.currentTime = 0;
s.play().catch(() => {});
}
function playWinSound() {
const s = document.getElementById("winSound");
s.currentTime = 0;
s.play().catch(() => {});
}
function updateStatus(text) { statusDiv.textContent = text; }
function updateUndoButton() {
undoBtn.disabled = gameOver || moveHistory.length === 0 || aiThinking;
}
// ========== 主题切换 ==========
function applyTheme(themeName) {
currentTheme = themes[themeName] || themes.classic;
document.body.style.background = currentTheme.background;
document.body.style.color = currentTheme.textColor;
document.querySelector("h1").style.color = currentTheme.textColor;
document.querySelector(".footer-info").style.color = currentTheme.textColor;
statusDiv.style.color = currentTheme.statusColor;
canvas.style.backgroundColor = currentTheme.canvasBg;
canvas.style.borderColor = currentTheme.canvasBorder;
canvas.style.boxShadow = currentTheme.shadow;
document.querySelectorAll("button").forEach(btn => {
btn.style.background = currentTheme.buttonBg;
btn.style.borderColor = currentTheme.buttonBorder;
btn.style.color = currentTheme.textColor;
});
document.querySelectorAll("select").forEach(sel => {
sel.style.background = currentTheme.buttonBg;
sel.style.borderColor = currentTheme.buttonBorder;
sel.style.color = currentTheme.textColor;
});
document.querySelector(".bottom-bar").style.borderTopColor = currentTheme.barBorder;
document.querySelector(".bottom-bar").style.background = currentTheme.background;
drawBoard();
}
document.getElementById("themeSelect").addEventListener("change", function() {
applyTheme(this.value);
});
// ========== 游戏核心 ==========
function init() {
for (let i = 0; i < boardSize; i++) {
chessBoard[i] = [];
for (let j = 0; j < boardSize; j++) chessBoard[i][j] = "";
}
moveHistory = [];
gameOver = false;
aiThinking = false;
if (aiTimeout) clearTimeout(aiTimeout);
currentPlayer = "black";
gameMode = document.getElementById("gameMode").value;
updateStatus(gameMode === "ai" ? "轮到你(黑棋)落子" : "轮到黑棋落子");
resizeCanvas();
updateUndoButton();
}
function restartGame() { init(); }
function drawBoard() {
if (cellSize === 0) return;
const size = cellSize * (boardSize + 1);
ctx.clearRect(0, 0, size, size);
// 网格线
ctx.strokeStyle = currentTheme.boardLines;
ctx.lineWidth = 1;
for (let i = 1; i <= boardSize; i++) {
ctx.beginPath();
ctx.moveTo(cellSize, i * cellSize);
ctx.lineTo(boardSize * cellSize, i * cellSize);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(i * cellSize, cellSize);
ctx.lineTo(i * cellSize, boardSize * cellSize);
ctx.stroke();
}
// 星位
ctx.fillStyle = currentTheme.starColor;
[[3,3],[11,3],[3,11],[11,11],[7,7]].forEach(([x,y]) => {
ctx.beginPath();
ctx.arc((x+1)*cellSize, (y+1)*cellSize, Math.max(3, cellSize*0.12), 0, Math.PI*2);
ctx.fill();
});
// 棋子
for (let i = 0; i < boardSize; i++)
for (let j = 0; j < boardSize; j++)
if (chessBoard[i][j]) drawChess(i, j, chessBoard[i][j]);
// 最后一步标记
if (moveHistory.length > 0) {
const last = moveHistory[moveHistory.length - 1];
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc((last.x+1)*cellSize, (last.y+1)*cellSize, cellSize*0.25, 0, Math.PI*2);
ctx.stroke();
}
}
function drawChess(x, y, color) {
const cx = (x+1)*cellSize, cy = (y+1)*cellSize;
const r = cellSize/2 - Math.max(1, cellSize*0.05);
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI*2);
const grad = ctx.createRadialGradient(cx-r/3, cy-r/3, 1, cx, cy, r);
if (color === "black") {
grad.addColorStop(0, currentTheme.blackGradient[0]);
grad.addColorStop(1, currentTheme.blackGradient[1]);
} else {
grad.addColorStop(0, currentTheme.whiteGradient[0]);
grad.addColorStop(1, currentTheme.whiteGradient[1]);
}
ctx.fillStyle = grad;
ctx.fill();
if (color === "white") {
ctx.strokeStyle = currentTheme.whiteBorder;
ctx.lineWidth = 1;
ctx.stroke();
}
}
// ========== 撤销 ==========
function undoMove() {
if (gameOver || moveHistory.length === 0 || aiThinking) return;
if (gameMode === "ai") {
const last = moveHistory[moveHistory.length - 1];
if (last.player === "white" && moveHistory.length >= 2) {
moveHistory.pop(); moveHistory.pop();
} else {
moveHistory.pop();
}
resetBoardFromHistory();
currentPlayer = "black";
updateStatus("轮到你(黑棋)落子");
} else {
moveHistory.pop();
resetBoardFromHistory();
currentPlayer = currentPlayer === "black" ? "white" : "black";
updateStatus(`轮到${currentPlayer === "black" ? "黑棋" : "白棋"}落子`);
}
drawBoard();
updateUndoButton();
}
function resetBoardFromHistory() {
for (let i = 0; i < boardSize; i++)
for (let j = 0; j < boardSize; j++) chessBoard[i][j] = "";
for (const m of moveHistory) chessBoard[m.x][m.y] = m.player;
}
// ========== 触控/点击落子 ==========
function handleInput(e) {
if (gameOver || aiThinking) return;
e.preventDefault(); // 防止触摸时触发鼠标事件
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.offsetWidth / (cellSize * (boardSize + 1));
const scaleY = canvas.offsetHeight / (cellSize * (boardSize + 1));
let clientX, clientY;
if (e.changedTouches && e.changedTouches.length > 0) {
clientX = e.changedTouches[0].clientX;
clientY = e.changedTouches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const x = Math.round((clientX - rect.left) / scaleX / cellSize) - 1;
const y = Math.round((clientY - rect.top) / scaleY / cellSize) - 1;
if (x >= 0 && x < boardSize && y >= 0 && y < boardSize && chessBoard[x][y] === "") {
chessBoard[x][y] = currentPlayer;
moveHistory.push({ x, y, player: currentPlayer });
drawBoard();
playPlaceSound();
if (checkWin(x, y)) {
const winner = currentPlayer === "black" ? "黑棋" : "白棋";
updateStatus(`${winner} 获胜!🎉`);
playWinSound();
gameOver = true;
if (gameMode === "ai") {
const key = currentPlayer === "black" ? "blackWins" : "whiteWins";
localStorage.setItem(key, parseInt(localStorage.getItem(key)) + 1);
updateScoreDisplay();
}
updateUndoButton();
return;
}
currentPlayer = currentPlayer === "black" ? "white" : "black";
if (gameMode === "ai" && currentPlayer === "white") {
updateStatus("🤔 电脑思考中...");
aiThinking = true;
updateUndoButton();
aiTimeout = setTimeout(aiMove, 500);
} else {
updateStatus(`轮到${currentPlayer === "black" ? "黑棋" : "白棋"}落子`);
}
updateUndoButton();
}
}
// 同时绑定touch和click,用preventDefault防重复
canvas.addEventListener("touchend", handleInput, { passive: false });
canvas.addEventListener("click", handleInput);
// ========== AI ==========
function aiMove() {
if (gameOver) { aiThinking = false; return; }
let bestScore = -Infinity, bestMove = null;
for (let i = 0; i < boardSize; i++)
for (let j = 0; j < boardSize; j++)
if (chessBoard[i][j] === "") {
const s = evaluate(i, j, "white");
if (s > bestScore) { bestScore = s; bestMove = {x:i, y:j}; }
}
if (bestMove) {
chessBoard[bestMove.x][bestMove.y] = "white";
moveHistory.push({ x: bestMove.x, y: bestMove.y, player: "white" });
drawBoard();
playPlaceSound();
if (checkWin(bestMove.x, bestMove.y)) {
updateStatus("电脑获胜 😢");
playWinSound();
gameOver = true;
localStorage.setItem("whiteWins", parseInt(localStorage.getItem("whiteWins")) + 1);
updateScoreDisplay();
aiThinking = false;
updateUndoButton();
return;
}
currentPlayer = "black";
updateStatus("轮到你(黑棋)落子");
}
aiThinking = false;
updateUndoButton();
}
// ========== 评估 & 胜负 ==========
const PS = { live5:100000, live4:10000, doubleLive3:5000, live3:1000, live2:100, block4:800, block3:50 };
function evaluate(x, y, player) {
let score = 0;
const dirs = [[[1,0],[-1,0]],[[0,1],[0,-1]],[[1,1],[-1,-1]],[[1,-1],[-1,1]]];
for (const dir of dirs) {
let line = "";
for (let s = -4; s <= 4; s++) {
const tx = x + dir[0][0]*s, ty = y + dir[0][1]*s;
line += (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize) ? (chessBoard[tx][ty]||"_") : "#";
}
line = line.replace(/_/i, "o");
if (line.includes("ooooo")) return PS.live5;
if (/(_|^)oooo_/.test(line)||/_oooo($|_)/.test(line)) score += PS.live4;
else if (/oo_o_o|o_oo_o|o_o_oo|_ooo_/.test(line)) score += PS.live3;
else if (/oo_oo/.test(line)) score += PS.block4;
else if (/oo_/.test(line)) score += PS.live2;
}
// 防守分
for (const dir of dirs) {
let line = "";
for (let s = -4; s <= 4; s++) {
const tx = x + dir[0][0]*s, ty = y + dir[0][1]*s;
line += (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize) ? (chessBoard[tx][ty]||"_") : "#";
}
line = line.replace(/_/i, "x");
if (line.includes("xxxxx")) score += PS.live5 * 0.9;
else if (/(_|^)xxxx_/.test(line)||/_xxxx($|_)/.test(line)) score += PS.live4 * 0.9;
else if (/xx_x_x|x_xx_x|x_x_xx|_xxx_/.test(line)) score += PS.live3 * 0.8;
}
return score;
}
function checkWin(x, y) {
const dirs = [[[1,0],[-1,0]],[[0,1],[0,-1]],[[1,1],[-1,-1]],[[1,-1],[-1,1]]];
const p = chessBoard[x][y];
for (const dir of dirs) {
let c = 1;
for (const d of dir) {
let tx = x+d[0], ty = y+d[1];
while (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize && chessBoard[tx][ty]===p) { c++; tx+=d[0]; ty+=d[1]; }
}
if (c >= 5) return true;
}
return false;
}
// ========== 启动 ==========
document.getElementById("gameMode").addEventListener("change", init);
initScores();
applyTheme("classic");
init();
</script>
</body>
</html>
index.html
md
README.md
index.html