<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>井字棋 | 战术系统</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 粒子庆祝层 -->
<div class="particles-container" id="particlesContainer"></div>
<div class="container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<h1 class="title" id="title">井 字 棋</h1>
<div class="toolbar-actions">
<!-- 主题切换 -->
<button class="icon-btn" id="themeBtn" title="切换主题">
<span id="themeIcon">◐</span>
</button>
<!-- 音效开关 -->
<button class="icon-btn" id="soundBtn" title="音效开关">
<span id="soundIcon">♪</span>
</button>
<!-- 游戏历史 -->
<button class="icon-btn" id="historyBtn" title="游戏历史">
▤
</button>
</div>
</div>
<!-- 模式选择 -->
<div class="mode-selector" id="modeSelector">
<button class="mode-btn active" data-mode="pvp">👥 双人对战</button>
<button class="mode-btn" data-mode="pve">🤖 人机对战</button>
</div>
<!-- 难度选择(仅人机模式) -->
<div class="difficulty-selector" id="difficultySelector">
<button class="diff-btn" data-diff="easy">😊 简单</button>
<button class="diff-btn active" data-diff="medium">🤔 中等</button>
<button class="diff-btn" data-diff="hard">😈 困难</button>
</div>
<!-- 得分板 -->
<div class="scoreboard">
<div class="player-score" id="playerX-score">
<span class="symbol">X</span>
<span class="player-label">玩家</span>
<span class="score-value" id="scoreX">0</span>
</div>
<div class="draw-score">
<span>平局</span>
<span class="score-value" id="scoreDraw">0</span>
</div>
<div class="player-score" id="playerO-score">
<span class="symbol">O</span>
<span class="player-label">玩家</span>
<span class="score-value" id="scoreO">0</span>
</div>
</div>
<!-- 计时器和回合信息 -->
<div class="info-bar">
<div class="timer" id="timer">⏱️ 00:00</div>
<div class="turn-indicator" id="turnIndicator">
<span class="turn-symbol">X</span> 的回合
</div>
<div class="move-count">第 <span id="moveCount">1</span> 步</div>
</div>
<!-- 游戏棋盘 -->
<div class="board" id="board">
<span class="corner-deco top-right"></span>
<span class="corner-deco bottom-left"></span>
<div class="cell" data-index="0"></div>
<div class="cell" data-index="1"></div>
<div class="cell" data-index="2"></div>
<div class="cell" data-index="3"></div>
<div class="cell" data-index="4"></div>
<div class="cell" data-index="5"></div>
<div class="cell" data-index="6"></div>
<div class="cell" data-index="7"></div>
<div class="cell" data-index="8"></div>
</div>
<!-- 状态消息 -->
<div class="message" id="message"></div>
<!-- 控制按钮 -->
<div class="controls">
<button class="btn btn-undo" id="undoBtn" title="悔棋 (Ctrl+Z)">↩ 悔棋</button>
<button class="btn btn-restart" id="restartBtn">🔄 重新开始</button>
<button class="btn btn-reset" id="resetBtn">🗑 重置比分</button>
</div>
</div>
<!-- 游戏历史侧面板 -->
<div class="history-panel" id="historyPanel">
<div class="history-header">
<h3>📋 游戏历史</h3>
<button class="close-btn" id="closeHistory">✕</button>
</div>
<div class="history-list" id="historyList"></div>
<div class="history-footer">
<button class="btn btn-clear-history" id="clearHistory">清空历史</button>
</div>
</div>
<div class="overlay" id="overlay"></div>
<script src="app.js"></script>
</body>
</html>
index.html
style.css
app.js
现在支持上传本地图片了!
index.html
style.css
/* ============ CSS 变量(科技感主题) ============ */
:root {
--bg-primary: #0a0e17;
--bg-secondary: rgba(0, 255, 255, 0.03);
--bg-cell: rgba(0, 255, 255, 0.02);
--bg-cell-hover: rgba(0, 255, 255, 0.08);
--bg-active: rgba(0, 255, 255, 0.08);
--border-color: rgba(0, 255, 255, 0.15);
--border-glow: rgba(0, 255, 255, 0.5);
--text-primary: #e0f7ff;
--text-secondary: rgba(0, 255, 255, 0.6);
--text-dim: rgba(0, 255, 255, 0.35);
--accent-cyan: #00ffff;
--accent-blue: #0088ff;
--accent-purple: #8800ff;
--accent-magenta: #ff00aa;
--accent-red: #ff0044;
--accent-green: #00ff88;
--accent-yellow: #ffcc00;
--shadow-color: rgba(0, 255, 255, 0.2);
--particle-color-1: #00ffff;
--particle-color-2: #0088ff;
--particle-color-3: #8800ff;
--particle-color-4: #ff00aa;
--particle-color-5: #00ff88;
--particle-color-6: #ffcc00;
--board-bg: rgba(0, 255, 255, 0.02);
--overlay-bg: rgba(0, 0, 0, 0.7);
--panel-bg: #0d1320;
--history-border: rgba(0, 255, 255, 0.1);
--glow-cyan: 0 0 15px rgba(0, 255, 255, 0.3), 0 0 30px rgba(0, 255, 255, 0.1);
--glow-red: 0 0 15px rgba(255, 0, 68, 0.3), 0 0 30px rgba(255, 0, 68, 0.1);
--glow-green: 0 0 15px rgba(0, 255, 136, 0.3), 0 0 30px rgba(0, 255, 136, 0.1);
}
/* ============ 全局样式 ============ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', 'Consolas', 'Monaco', 'SF Mono', monospace;
background: var(--bg-primary);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: var(--text-primary);
overflow-x: hidden;
position: relative;
}
/* 背景网格 */
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-image:
linear-gradient(rgba(0, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 255, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
z-index: 0;
pointer-events: none;
}
/* 扫描线效果 */
body::after {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 255, 0.008) 2px,
rgba(0, 255, 255, 0.008) 4px
);
z-index: 0;
pointer-events: none;
}
/* ============ 粒子容器 ============ */
.particles-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
overflow: hidden;
}
/* ============ 主容器 ============ */
.container {
text-align: center;
padding: 20px;
max-width: 460px;
width: 100%;
position: relative;
z-index: 1;
}
/* ============ 工具栏 ============ */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.title {
font-size: 1.8rem;
letter-spacing: 10px;
text-transform: uppercase;
color: var(--accent-cyan);
text-shadow: 0 0 20px rgba(0, 255, 255, 0.5), 0 0 40px rgba(0, 255, 255, 0.2);
position: relative;
}
.title::after {
content: '';
position: absolute;
bottom: -6px;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
}
.toolbar-actions {
display: flex;
gap: 6px;
}
.icon-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px 10px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-family: inherit;
}
.icon-btn:hover {
background: var(--bg-active);
border-color: var(--border-glow);
box-shadow: var(--glow-cyan);
color: var(--accent-cyan);
transform: scale(1.1);
}
/* ============ 模式选择 ============ */
.mode-selector {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 10px;
}
.mode-btn {
padding: 8px 18px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: inherit;
letter-spacing: 1px;
text-transform: uppercase;
position: relative;
overflow: hidden;
}
.mode-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.05), transparent);
transition: left 0.5s ease;
}
.mode-btn:hover::before {
left: 100%;
}
.mode-btn:hover {
border-color: var(--border-glow);
color: var(--accent-cyan);
}
.mode-btn.active {
background: linear-gradient(135deg, rgba(0, 255, 255, 0.15), rgba(0, 136, 255, 0.15));
color: var(--accent-cyan);
border-color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
/* ============ 难度选择 ============ */
.difficulty-selector {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 14px;
transition: all 0.3s ease;
}
.difficulty-selector.hidden {
opacity: 0;
pointer-events: none;
height: 0;
margin: 0;
overflow: hidden;
}
.diff-btn {
padding: 5px 14px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: inherit;
letter-spacing: 1px;
text-transform: uppercase;
}
.diff-btn:hover {
border-color: var(--border-glow);
color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
.diff-btn.active {
background: linear-gradient(135deg, rgba(136, 0, 255, 0.2), rgba(0, 136, 255, 0.2));
color: var(--accent-purple);
border-color: var(--accent-purple);
box-shadow: 0 0 15px rgba(136, 0, 255, 0.3), 0 0 30px rgba(136, 0, 255, 0.1);
}
/* ============ 得分板 ============ */
.scoreboard {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 14px;
}
.player-score {
background: var(--bg-secondary);
border-radius: 4px;
padding: 8px 16px;
min-width: 85px;
border: 1px solid var(--border-color);
position: relative;
transition: all 0.3s ease;
}
.player-score::before {
content: '';
position: absolute;
top: -1px;
left: 20%;
width: 60%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-glow), transparent);
}
.player-score.active {
background: var(--bg-active);
border-color: var(--border-glow);
box-shadow: var(--glow-cyan);
transform: scale(1.05);
}
.player-score .symbol {
font-size: 1.2rem;
font-weight: 700;
display: block;
}
.player-score .player-label {
font-size: 0.6rem;
color: var(--text-dim);
display: block;
margin-top: 2px;
letter-spacing: 2px;
text-transform: uppercase;
}
.player-score .score-value {
font-size: 1.5rem;
font-weight: 700;
display: block;
margin-top: 4px;
font-variant-numeric: tabular-nums;
}
.draw-score {
background: var(--bg-secondary);
border-radius: 4px;
padding: 8px 14px;
min-width: 70px;
border: 1px solid var(--border-color);
position: relative;
transition: all 0.3s ease;
}
.draw-score::before {
content: '';
position: absolute;
top: -1px;
left: 20%;
width: 60%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-glow), transparent);
}
.draw-score span:first-child {
font-size: 0.65rem;
color: var(--text-dim);
display: block;
letter-spacing: 2px;
text-transform: uppercase;
}
.draw-score .score-value {
font-size: 1.5rem;
font-weight: 700;
display: block;
margin-top: 4px;
font-variant-numeric: tabular-nums;
}
/* ============ 信息栏 ============ */
.info-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
padding: 0 4px;
}
.timer {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
letter-spacing: 1px;
}
.timer::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background: var(--accent-green);
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
animation: blink 2s ease infinite;
box-shadow: 0 0 8px var(--accent-green);
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.turn-indicator {
font-size: 1rem;
font-weight: 600;
flex-grow: 1;
text-align: center;
letter-spacing: 1px;
}
.turn-indicator .turn-symbol {
font-weight: 800;
}
.turn-indicator .turn-symbol.x {
color: var(--accent-red);
text-shadow: 0 0 10px rgba(255, 0, 68, 0.5);
}
.turn-indicator .turn-symbol.o {
color: var(--accent-blue);
text-shadow: 0 0 10px rgba(0, 136, 255, 0.5);
}
.move-count {
font-size: 0.75rem;
color: var(--text-dim);
font-weight: 600;
letter-spacing: 1px;
}
/* ============ 棋盘 ============ */
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
width: 320px;
height: 320px;
margin: 0 auto 16px;
padding: 10px;
background: var(--board-bg);
border-radius: 8px;
border: 1px solid var(--border-color);
position: relative;
}
/* 棋盘四角装饰 */
.board::before,
.board::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border: 2px solid var(--accent-cyan);
z-index: 2;
}
.board::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
box-shadow: -2px -2px 10px rgba(0, 255, 255, 0.3);
}
.board::after {
bottom: -1px;
right: -1px;
border-left: none;
border-top: none;
box-shadow: 2px 2px 10px rgba(0, 255, 255, 0.3);
}
/* 棋盘额外角标 */
.board .corner-deco {
position: absolute;
width: 20px;
height: 20px;
border: 2px solid var(--accent-cyan);
z-index: 2;
}
.board .corner-deco.top-right {
top: -1px;
right: -1px;
border-left: none;
border-bottom: none;
box-shadow: 2px -2px 10px rgba(0, 255, 255, 0.3);
}
.board .corner-deco.bottom-left {
bottom: -1px;
left: -1px;
border-right: none;
border-top: none;
box-shadow: -2px 2px 10px rgba(0, 255, 255, 0.3);
}
.cell {
background: var(--bg-cell);
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2.8rem;
font-weight: 800;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(0, 255, 255, 0.06);
user-select: none;
position: relative;
font-family: 'Courier New', 'Consolas', 'Monaco', monospace;
}
.cell:hover:not(.taken) {
background: var(--bg-cell-hover);
border-color: rgba(0, 255, 255, 0.25);
box-shadow: inset 0 0 20px rgba(0, 255, 255, 0.05);
}
.cell.taken {
cursor: default;
}
.cell .cell-x {
color: var(--accent-red);
text-shadow: 0 0 15px rgba(255, 0, 68, 0.6), 0 0 30px rgba(255, 0, 68, 0.3);
animation: techPopIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.cell .cell-o {
color: var(--accent-blue);
text-shadow: 0 0 15px rgba(0, 136, 255, 0.6), 0 0 30px rgba(0, 136, 255, 0.3);
animation: techPopIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.cell.winner-cell {
background: rgba(0, 255, 136, 0.08);
border-color: var(--accent-green);
animation: techWinPulse 1s ease infinite;
box-shadow: inset 0 0 20px rgba(0, 255, 136, 0.1), 0 0 15px rgba(0, 255, 136, 0.2);
}
@keyframes techPopIn {
0% {
transform: scale(0);
opacity: 0;
filter: blur(4px);
}
50% {
filter: blur(0);
}
100% {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
@keyframes techWinPulse {
0%, 100% {
box-shadow: inset 0 0 20px rgba(0, 255, 136, 0.1), 0 0 10px rgba(0, 255, 136, 0.2);
}
50% {
box-shadow: inset 0 0 30px rgba(0, 255, 136, 0.2), 0 0 25px rgba(0, 255, 136, 0.5);
}
}
/* ============ 消息 ============ */
.message {
font-size: 1.1rem;
min-height: 36px;
margin-bottom: 14px;
font-weight: 600;
opacity: 0;
transition: opacity 0.3s ease;
letter-spacing: 2px;
text-transform: uppercase;
}
.message.show { opacity: 1; }
.message.winner {
color: var(--accent-green);
text-shadow: 0 0 20px rgba(0, 255, 136, 0.5), 0 0 40px rgba(0, 255, 136, 0.2);
animation: glitchText 0.5s ease;
}
.message.draw {
color: var(--text-dim);
}
@keyframes glitchText {
0% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(2px, -2px); }
60% { transform: translate(-1px, -1px); }
80% { transform: translate(1px, 1px); }
100% { transform: translate(0); }
}
/* ============ 按钮 ============ */
.controls {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
letter-spacing: 1px;
text-transform: uppercase;
font-family: inherit;
color: var(--text-primary);
background: var(--bg-secondary);
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.05), transparent);
transition: left 0.5s ease;
}
.btn:hover::before {
left: 100%;
}
.btn-undo {
border-color: rgba(136, 0, 255, 0.4);
color: var(--accent-purple);
}
.btn-undo:hover:not(:disabled) {
border-color: var(--accent-purple);
box-shadow: 0 0 15px rgba(136, 0, 255, 0.3), 0 0 30px rgba(136, 0, 255, 0.1);
transform: translateY(-2px);
}
.btn-restart {
border-color: rgba(0, 255, 255, 0.4);
color: var(--accent-cyan);
}
.btn-restart:hover {
border-color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
transform: translateY(-2px);
}
.btn-reset {
border-color: rgba(255, 0, 68, 0.3);
color: var(--accent-red);
}
.btn-reset:hover {
border-color: var(--accent-red);
box-shadow: var(--glow-red);
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0) !important;
}
.btn:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: none !important;
}
.btn-clear-history {
border-color: rgba(255, 0, 68, 0.3);
color: var(--accent-red);
margin-top: 8px;
width: 100%;
}
/* ============ 粒子 ============ */
.particle {
position: absolute;
pointer-events: none;
animation: particleFall linear forwards;
}
@keyframes particleFall {
0% {
opacity: 1;
transform: translateY(0) rotate(0deg) scale(1);
}
80% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateY(100vh) rotate(360deg) scale(0.1);
}
}
/* ============ 侧边面板(历史) ============ */
.overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--overlay-bg);
z-index: 900;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.overlay.active {
opacity: 1;
pointer-events: auto;
}
.history-panel {
position: fixed;
top: 0;
right: -380px;
width: 360px;
max-width: 90vw;
height: 100vh;
background: var(--panel-bg);
border-left: 1px solid var(--border-color);
z-index: 1001;
transition: right 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
display: flex;
flex-direction: column;
box-shadow: -5px 0 30px rgba(0, 255, 255, 0.1);
}
.history-panel.active {
right: 0;
}
.history-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--history-border);
}
.history-header h3 {
font-size: 1rem;
color: var(--accent-cyan);
letter-spacing: 3px;
text-transform: uppercase;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.close-btn {
background: none;
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 1.1rem;
cursor: pointer;
padding: 4px 10px;
border-radius: 4px;
transition: all 0.3s ease;
font-family: inherit;
}
.close-btn:hover {
background: var(--bg-active);
color: var(--accent-red);
border-color: var(--accent-red);
}
.history-list {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
.history-item {
padding: 12px;
border-radius: 4px;
background: var(--bg-secondary);
margin-bottom: 8px;
border: 1px solid var(--history-border);
font-size: 0.75rem;
transition: all 0.3s ease;
font-family: 'Courier New', monospace;
}
.history-item:hover {
background: var(--bg-active);
border-color: var(--border-glow);
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.history-item-header .result {
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 1px;
}
.history-item-header .time {
color: var(--text-dim);
font-size: 0.7rem;
}
.history-item .moves-preview {
color: var(--text-secondary);
font-family: 'Courier New', monospace;
font-size: 0.75rem;
word-break: break-all;
line-height: 1.6;
letter-spacing: 1px;
}
.history-empty {
text-align: center;
color: var(--text-dim);
padding: 40px 20px;
font-size: 0.85rem;
line-height: 1.8;
letter-spacing: 1px;
}
.history-footer {
padding: 12px 16px 20px;
border-top: 1px solid var(--history-border);
}
/* ============ 颜色类 ============ */
.color-X { color: var(--accent-red); text-shadow: 0 0 8px rgba(255, 0, 68, 0.4); }
.color-O { color: var(--accent-blue); text-shadow: 0 0 8px rgba(0, 136, 255, 0.4); }
/* ============ 明亮科技感主题 ============ */
.light-theme {
--bg-primary: #e8eef5;
--bg-secondary: rgba(0, 100, 200, 0.04);
--bg-cell: rgba(0, 100, 200, 0.03);
--bg-cell-hover: rgba(0, 100, 200, 0.08);
--bg-active: rgba(0, 100, 200, 0.08);
--border-color: rgba(0, 100, 200, 0.2);
--border-glow: rgba(0, 100, 200, 0.5);
--text-primary: #1a2a3a;
--text-secondary: rgba(0, 80, 180, 0.6);
--text-dim: rgba(0, 80, 180, 0.35);
--accent-cyan: #0077bb;
--accent-blue: #0055aa;
--accent-purple: #6600cc;
--accent-magenta: #cc0088;
--accent-red: #cc0033;
--accent-green: #00aa55;
--accent-yellow: #aa8800;
--board-bg: rgba(0, 100, 200, 0.03);
--overlay-bg: rgba(0, 0, 0, 0.4);
--panel-bg: #f0f4fa;
--history-border: rgba(0, 100, 200, 0.12);
--glow-cyan: 0 0 10px rgba(0, 100, 200, 0.2), 0 0 20px rgba(0, 100, 200, 0.08);
--glow-red: 0 0 10px rgba(204, 0, 51, 0.2), 0 0 20px rgba(204, 0, 51, 0.08);
--glow-green: 0 0 10px rgba(0, 170, 85, 0.2), 0 0 20px rgba(0, 170, 85, 0.08);
}
.light-theme body::before {
background-image:
linear-gradient(rgba(0, 100, 200, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 100, 200, 0.04) 1px, transparent 1px);
}
.light-theme .board::before {
border-color: var(--accent-cyan);
}
.light-theme .board::after {
border-color: var(--accent-cyan);
}
.light-theme .board .corner-deco {
border-color: var(--accent-cyan);
}
/* ============ 滚动条美化 ============ */
.history-list::-webkit-scrollbar {
width: 4px;
}
.history-list::-webkit-scrollbar-track {
background: transparent;
}
.history-list::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 2px;
}
.history-list::-webkit-scrollbar-thumb:hover {
background: var(--border-glow);
}
/* ============ 响应式 ============ */
@media (max-width: 400px) {
.title {
font-size: 1.3rem;
letter-spacing: 6px;
}
.board {
width: 260px;
height: 260px;
}
.cell {
font-size: 2.2rem;
}
.scoreboard {
gap: 8px;
}
.player-score,
.draw-score {
padding: 6px 10px;
min-width: 65px;
}
.controls {
gap: 8px;
}
.btn {
padding: 9px 12px;
font-size: 0.7rem;
}
.mode-btn {
padding: 6px 12px;
font-size: 0.7rem;
}
.history-panel {
width: 300px;
}
}
编辑器加载中
app.js
/**
* 井字棋游戏 - 科技感风格
* 功能:双人对战、人机对战(3档AI)、悔棋、计时、音效、粒子、历史、主题
*/
// ============ 游戏状态 ============
const state = {
board: Array(9).fill(''),
currentPlayer: 'X',
gameActive: true,
scores: { X: 0, O: 0, draw: 0 },
moveHistory: [],
moveCount: 1,
timerInterval: null,
seconds: 0,
mode: 'pvp',
difficulty: 'medium',
isLightTheme: false,
soundEnabled: true,
history: [],
aiThinking: false,
};
// 获胜组合
const WIN_COMBOS = [
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6],
];
// ============ DOM 元素 ============
const cells = document.querySelectorAll('.cell');
const turnIndicator = document.getElementById('turnIndicator');
const messageEl = document.getElementById('message');
const scoreXEl = document.getElementById('scoreX');
const scoreOEl = document.getElementById('scoreO');
const scoreDrawEl = document.getElementById('scoreDraw');
const playerXScoreEl = document.getElementById('playerX-score');
const playerOScoreEl = document.getElementById('playerO-score');
const timerEl = document.getElementById('timer');
const moveCountEl = document.getElementById('moveCount');
const restartBtn = document.getElementById('restartBtn');
const resetBtn = document.getElementById('resetBtn');
const undoBtn = document.getElementById('undoBtn');
const themeBtn = document.getElementById('themeBtn');
const themeIcon = document.getElementById('themeIcon');
const soundBtn = document.getElementById('soundBtn');
const soundIcon = document.getElementById('soundIcon');
const historyBtn = document.getElementById('historyBtn');
const historyPanel = document.getElementById('historyPanel');
const historyList = document.getElementById('historyList');
const closeHistory = document.getElementById('closeHistory');
const clearHistory = document.getElementById('clearHistory');
const overlay = document.getElementById('overlay');
const particlesContainer = document.getElementById('particlesContainer');
const modeSelector = document.getElementById('modeSelector');
const difficultySelector = document.getElementById('difficultySelector');
// ============ 音效系统(Web Audio API) ============
let audioCtx = null;
function getAudioCtx() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
function playTone(frequency, duration, type = 'sine', volume = 0.15) {
if (!state.soundEnabled) return;
try {
const ctx = getAudioCtx();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(frequency, ctx.currentTime);
gain.gain.setValueAtTime(volume, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + duration);
} catch (e) { /* 忽略音频错误 */ }
}
function playMoveSound() {
playTone(1200, 0.06, 'square', 0.06);
playTone(800, 0.08, 'sine', 0.08);
}
function playWinSound() {
playTone(523, 0.12, 'sine', 0.12);
setTimeout(() => playTone(659, 0.12, 'sine', 0.12), 120);
setTimeout(() => playTone(784, 0.12, 'sine', 0.12), 240);
setTimeout(() => playTone(1047, 0.3, 'sine', 0.15), 360);
}
function playDrawSound() {
playTone(400, 0.2, 'triangle', 0.1);
setTimeout(() => playTone(350, 0.25, 'triangle', 0.1), 180);
}
function playUndoSound() {
playTone(700, 0.06, 'sine', 0.08);
setTimeout(() => playTone(500, 0.08, 'sine', 0.06), 80);
}
// ============ 粒子系统 ============
function spawnParticles(count = 80) {
const colors = [
'#00ffff',
'#0088ff',
'#8800ff',
'#ff00aa',
'#00ff88',
'#ffcc00',
];
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.classList.add('particle');
const size = Math.random() * 8 + 3;
const color = colors[Math.floor(Math.random() * colors.length)];
const left = Math.random() * 100;
const delay = Math.random() * 1.5;
const duration = Math.random() * 2 + 1.5;
particle.style.cssText = `
width: ${size}px;
height: ${size}px;
background: ${color};
left: ${left}%;
top: -${size}px;
animation-delay: ${delay}s;
animation-duration: ${duration}s;
box-shadow: 0 0 ${size * 2}px ${color};
`;
particlesContainer.appendChild(particle);
setTimeout(() => particle.remove(), (delay + duration) * 1000);
}
}
// ============ 计时器 ============
function startTimer() {
stopTimer();
state.seconds = 0;
updateTimerDisplay();
state.timerInterval = setInterval(() => {
state.seconds++;
updateTimerDisplay();
}, 1000);
}
function stopTimer() {
if (state.timerInterval) {
clearInterval(state.timerInterval);
state.timerInterval = null;
}
}
function updateTimerDisplay() {
const m = String(Math.floor(state.seconds / 60)).padStart(2, '0');
const s = String(state.seconds % 60).padStart(2, '0');
timerEl.textContent = `⏱️ ${m}:${s}`;
}
// ============ 初始化 ============
function init() {
cells.forEach(cell => {
cell.addEventListener('click', handleCellClick);
});
restartBtn.addEventListener('click', restartGame);
resetBtn.addEventListener('click', resetScores);
undoBtn.addEventListener('click', undoMove);
themeBtn.addEventListener('click', toggleTheme);
soundBtn.addEventListener('click', toggleSound);
historyBtn.addEventListener('click', openHistory);
closeHistory.addEventListener('click', closeHistoryPanel);
overlay.addEventListener('click', closeHistoryPanel);
clearHistory.addEventListener('click', clearAllHistory);
modeSelector.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
modeSelector.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.mode = btn.dataset.mode;
updateModeUI();
restartGame();
});
});
difficultySelector.querySelectorAll('.diff-btn').forEach(btn => {
btn.addEventListener('click', () => {
difficultySelector.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.difficulty = btn.dataset.diff;
if (state.mode === 'pve') restartGame();
});
});
document.addEventListener('keydown', handleKeyboard);
updateModeUI();
updateActivePlayerHighlight();
}
// ============ 键盘快捷键 ============
function handleKeyboard(e) {
const keyMap = {
'1': 6, '2': 7, '3': 8,
'4': 3, '5': 4, '6': 5,
'7': 0, '8': 1, '9': 2,
'q': 0, 'w': 1, 'e': 2,
'a': 3, 's': 4, 'd': 5,
'z': 6, 'x': 7, 'c': 8,
};
const cellIndex = keyMap[e.key.toLowerCase()];
if (cellIndex !== undefined) {
const cell = cells[cellIndex];
if (cell) handleCellClick({ target: cell });
}
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
undoMove();
}
}
// ============ 处理点击 ============
function handleCellClick(e) {
const cell = e.target.closest('.cell');
if (!cell) return;
const index = parseInt(cell.dataset.index);
if (state.board[index] !== '' || !state.gameActive || state.aiThinking) return;
if (state.mode === 'pve' && state.currentPlayer === 'O') return;
makeMove(index);
}
// ============ 执行落子 ============
function makeMove(index) {
const player = state.currentPlayer;
state.board[index] = player;
state.moveHistory.push({ cell: index, player });
renderCell(cells[index], player);
playMoveSound();
const winResult = checkWin();
if (winResult) { endGame(winResult); return; }
if (state.board.every(c => c !== '')) { endGame('draw'); return; }
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.moveCount++;
moveCountEl.textContent = state.moveCount;
updateTurnDisplay();
updateActivePlayerHighlight();
updateUndoButton();
if (state.mode === 'pve' && state.currentPlayer === 'O' && state.gameActive) {
state.aiThinking = true;
updateTurnDisplay();
setTimeout(() => {
const aiMove = getAIMove();
state.aiThinking = false;
if (aiMove !== -1) makeMove(aiMove);
}, 400 + Math.random() * 300);
}
}
// ============ 渲染单元格 ============
function renderCell(cell, player) {
cell.classList.add('taken');
const span = document.createElement('span');
span.classList.add(player === 'X' ? 'cell-x' : 'cell-o');
span.textContent = player;
cell.appendChild(span);
}
// ============ 更新回合显示 ============
function updateTurnDisplay() {
if (!state.gameActive) {
const allEmpty = state.board.every(c => c === '');
turnIndicator.textContent = allEmpty ? '等待开始' : '';
return;
}
const colorClass = state.currentPlayer === 'X' ? 'x' : 'o';
let labelText = `${state.currentPlayer} 的回合`;
if (state.mode === 'pve' && state.currentPlayer === 'O') {
labelText = '🤖 AI 思考中';
}
turnIndicator.innerHTML = `<span class="turn-symbol ${colorClass}">${labelText}</span>`;
}
// ============ 高亮当前玩家 ============
function updateActivePlayerHighlight() {
const isPlayerXTurn = state.currentPlayer === 'X';
const isPlayerOTurn = state.currentPlayer === 'O';
playerXScoreEl.classList.toggle('active', isPlayerXTurn && state.gameActive);
playerOScoreEl.classList.toggle('active', isPlayerOTurn && state.gameActive);
}
// ============ 检查胜负 ============
function checkWin() {
for (const combo of WIN_COMBOS) {
const [a, b, c] = combo;
if (state.board[a] !== '' && state.board[a] === state.board[b] && state.board[a] === state.board[c]) {
return { winner: state.board[a], combo };
}
}
return null;
}
// ============ 游戏结束 ============
function endGame(result) {
state.gameActive = false;
stopTimer();
if (result === 'draw') {
state.scores.draw++;
scoreDrawEl.textContent = state.scores.draw;
messageEl.textContent = '⊘ 平局 — 比赛结束';
messageEl.className = 'message show draw';
playDrawSound();
} else {
state.scores[result.winner]++;
updateScoreDisplay();
highlightWinningCells(result.combo);
if (state.mode === 'pve') {
if (result.winner === 'X') {
messageEl.textContent = '▸ 你赢了!目标已消灭';
} else {
messageEl.textContent = '▸ AI 获胜!系统主宰';
}
} else {
messageEl.textContent = `▸ ${result.winner} 获胜!消灭成功`;
}
messageEl.className = 'message show winner';
playWinSound();
spawnParticles(80);
}
saveGameResult(result);
updateUndoButton();
}
// ============ 高亮获胜格子 ============
function highlightWinningCells(combo) {
combo.forEach(index => {
cells[index].classList.add('winner-cell');
});
}
// ============ 更新分数 ============
function updateScoreDisplay() {
scoreXEl.textContent = state.scores.X;
scoreOEl.textContent = state.scores.O;
}
// ============ AI 逻辑 ============
function getAIMove() {
const emptyCells = state.board.map((v, i) => v === '' ? i : -1).filter(i => i !== -1);
if (emptyCells.length === 0) return -1;
switch (state.difficulty) {
case 'easy': return easyAI(emptyCells);
case 'medium': return mediumAI();
case 'hard': return hardAI();
default: return easyAI(emptyCells);
}
}
function easyAI(emptyCells) {
return emptyCells[Math.floor(Math.random() * emptyCells.length)];
}
function mediumAI() {
const winMove = findWinningMove('O');
if (winMove !== -1) return winMove;
const blockMove = findWinningMove('X');
if (blockMove !== -1) return blockMove;
if (state.board[4] === '') return 4;
const emptyCells = state.board.map((v, i) => v === '' ? i : -1).filter(i => i !== -1);
return emptyCells[Math.floor(Math.random() * emptyCells.length)];
}
function hardAI() {
let bestScore = -Infinity;
let bestMove = -1;
for (let i = 0; i < 9; i++) {
if (state.board[i] === '') {
state.board[i] = 'O';
let score = minimax(state.board, 0, false);
state.board[i] = '';
if (score > bestScore) { bestScore = score; bestMove = i; }
}
}
return bestMove;
}
function minimax(board, depth, isMaximizing) {
const winner = getWinner(board);
if (winner === 'O') return 10 - depth;
if (winner === 'X') return depth - 10;
if (board.every(c => c !== '')) return 0;
if (isMaximizing) {
let best = -Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] === '') {
board[i] = 'O';
best = Math.max(best, minimax(board, depth + 1, false));
board[i] = '';
}
}
return best;
} else {
let best = Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] === '') {
board[i] = 'X';
best = Math.min(best, minimax(board, depth + 1, true));
board[i] = '';
}
}
return best;
}
}
function getWinner(board) {
for (const [a, b, c] of WIN_COMBOS) {
if (board[a] && board[a] === board[b] && board[a] === board[c]) return board[a];
}
return null;
}
function findWinningMove(player) {
for (const combo of WIN_COMBOS) {
const [a, b, c] = combo;
const values = [state.board[a], state.board[b], state.board[c]];
const playerCount = values.filter(v => v === player).length;
const emptyCount = values.filter(v => v === '').length;
if (playerCount === 2 && emptyCount === 1) {
return combo[values.indexOf('')];
}
}
return -1;
}
// ============ 悔棋 ============
function undoMove() {
if (state.moveHistory.length === 0 || !state.gameActive) return;
if (state.mode === 'pve') {
if (state.moveHistory.length >= 2) {
const last = state.moveHistory.pop();
state.board[last.cell] = '';
renderCellUndo(cells[last.cell]);
const secondLast = state.moveHistory.pop();
state.board[secondLast.cell] = '';
renderCellUndo(cells[secondLast.cell]);
state.currentPlayer = 'X';
state.moveCount = Math.max(1, state.moveCount - 2);
} else {
const last = state.moveHistory.pop();
state.board[last.cell] = '';
renderCellUndo(cells[last.cell]);
state.currentPlayer = 'X';
state.moveCount = Math.max(1, state.moveCount - 1);
}
} else {
const last = state.moveHistory.pop();
state.board[last.cell] = '';
renderCellUndo(cells[last.cell]);
state.currentPlayer = last.player;
state.moveCount = Math.max(1, state.moveCount - 1);
}
playUndoSound();
updateTurnDisplay();
updateActivePlayerHighlight();
updateUndoButton();
}
function renderCellUndo(cell) {
cell.innerHTML = '';
cell.classList.remove('taken', 'winner-cell');
}
function updateUndoButton() {
undoBtn.disabled = state.moveHistory.length === 0 || !state.gameActive;
}
// ============ 重新开始 ============
function restartGame() {
state.board = Array(9).fill('');
state.currentPlayer = 'X';
state.gameActive = true;
state.moveHistory = [];
state.moveCount = 1;
moveCountEl.textContent = '1';
cells.forEach(cell => {
cell.innerHTML = '';
cell.classList.remove('taken', 'winner-cell');
});
messageEl.className = 'message';
messageEl.textContent = '';
updateTurnDisplay();
updateActivePlayerHighlight();
updateUndoButton();
startTimer();
}
// ============ 重置比分 ============
function resetScores() {
state.scores = { X: 0, O: 0, draw: 0 };
scoreXEl.textContent = '0';
scoreOEl.textContent = '0';
scoreDrawEl.textContent = '0';
restartGame();
}
// ============ 主题切换 ============
function toggleTheme() {
state.isLightTheme = !state.isLightTheme;
document.body.classList.toggle('light-theme', state.isLightTheme);
themeIcon.textContent = state.isLightTheme ? '◑' : '◐';
playTone(500, 0.05, 'sine', 0.08);
}
// ============ 音效开关 ============
function toggleSound() {
state.soundEnabled = !state.soundEnabled;
soundIcon.textContent = state.soundEnabled ? '♪' : '♭';
if (state.soundEnabled) playTone(600, 0.08, 'sine', 0.1);
}
// ============ 游戏历史记录 ============
function saveGameResult(result) {
const entry = {
time: new Date().toLocaleString('zh-CN'),
mode: state.mode === 'pvp' ? '双人对战' : `人机对战[${state.difficulty}]`,
result: result === 'draw' ? '平局' : `${result.winner} 获胜`,
winner: result.winner,
duration: state.seconds,
moves: [...state.moveHistory],
totalMoves: state.moveCount,
};
state.history.unshift(entry);
if (state.history.length > 50) state.history.pop();
}
function openHistory() {
renderHistoryList();
historyPanel.classList.add('active');
overlay.classList.add('active');
}
function closeHistoryPanel() {
historyPanel.classList.remove('active');
overlay.classList.remove('active');
}
function renderHistoryList() {
if (state.history.length === 0) {
historyList.innerHTML = '<div class="history-empty">▾ 暂无数据 ▾<br>// 等待首次匹配记录...</div>';
return;
}
historyList.innerHTML = state.history.map(h => {
const resultClass = h.winner === 'X' ? 'color-X' : h.winner === 'O' ? 'color-O' : '';
const m = Math.floor(h.duration / 60);
const s = h.duration % 60;
const movesText = h.moves.map(mv => `${mv.player}${mv.cell + 1}`).join(' → ');
return `
<div class="history-item">
<div class="history-item-header">
<span class="result ${resultClass}">${h.result}</span>
<span class="time">// ${h.time} | ${m}:${String(s).padStart(2,'0')}</span>
</div>
<div class="moves-preview">> ${movesText}</div>
<div style="margin-top:4px;font-size:0.7rem;color:var(--text-dim)">模式: ${h.mode} | 步数: ${h.totalMoves}</div>
</div>
`;
}).join('');
}
function clearAllHistory() {
state.history = [];
renderHistoryList();
playTone(300, 0.1, 'sine', 0.1);
}
// ============ 模式 UI 更新 ============
function updateModeUI() {
const isPVE = state.mode === 'pve';
difficultySelector.classList.toggle('hidden', !isPVE);
const labelX = playerXScoreEl.querySelector('.player-label');
const labelO = playerOScoreEl.querySelector('.player-label');
if (state.mode === 'pve') {
labelX.textContent = '玩家';
labelO.textContent = 'AI';
} else {
labelX.textContent = '玩家';
labelO.textContent = '玩家';
}
}
// ============ 启动游戏 ============
init();
编辑器加载中
预览页面