五子棋-html版本

  • 游戏模式:人机对战(PVE)和人人对战(PVP)。

  • 难度选择:简单、中等、困难,影响AI强度。

  • 棋盘与棋子:15x15棋盘,可显示坐标。

  • 控制功能:重新开始、悔棋、切换坐标显示。

  • 颜色自定义:可更改黑棋、白棋、棋盘背景、窗口背景颜色。

  • 棋谱管理:保存当前对局为JSON文件,加载棋谱进行复盘。

  • 复盘模式:加载棋谱后可逐步查看每一步,前进后退,退出复盘。

  • 界面布局:右侧面板包含所有控制按钮,棋盘在左侧。

  • 支持中文显示,若字体文件存在则使用自定义字体,否则回退系统字体。

    <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>五子棋 · 宽格版(修复悔棋BUG)</title> <style> * { box-sizing: border-box; user-select: none; margin: 0; padding: 0; }
    复制代码
          body {
              background: #2c3e4f;
              display: flex;
              justify-content: center;
              align-items: center;
              min-height: 100vh;
              font-family: 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', sans-serif;
              padding: 20px;
          }
    
          .game-layout {
              display: flex;
              align-items: stretch;
              justify-content: center;
              gap: 28px;
              width: 100%;
              max-width: 1800px;
              margin: 0 auto;
          }
    
          .control-panel {
              width: 280px;
              background: rgba(35, 35, 45, 0.85);
              backdrop-filter: blur(10px);
              -webkit-backdrop-filter: blur(10px);
              border-radius: 48px;
              padding: 30px 20px;
              color: #f0e9e0;
              box-shadow: 0 20px 30px -8px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,215,140,0.2) inset;
              border: 1px solid rgba(255, 215, 150, 0.3);
              display: flex;
              flex-direction: column;
              gap: 18px;
              max-height: 90vh;
              overflow-y: auto;
              scrollbar-width: thin;
              scrollbar-color: #c09a6b #3a2a2a;
          }
    
          .control-panel::-webkit-scrollbar {
              width: 6px;
          }
          .control-panel::-webkit-scrollbar-thumb {
              background: #c09a6b;
              border-radius: 20px;
          }
    
          .board-wrapper {
              background: transparent;
              padding: 0;
              border-radius: 36px;
              box-shadow: 0 25px 40px rgba(0, 0, 0, 0.5);
              display: flex;
              align-items: center;
              justify-content: center;
              flex: 0 0 auto;
          }
    
          canvas#boardCanvas {
              display: block;
              width: min(85vh, 85vw);
              height: min(85vh, 85vw);
              background: #ebc28e;
              border-radius: 32px;
              cursor: pointer;
              box-shadow: inset 0 0 0 2px #b87c3a, 0 15px 25px rgba(0,0,0,0.4);
              transition: background-color 0.3s ease;
          }
    
          canvas#boardCanvas.victory-gray {
              background: #b0b0b0 !important;
          }
    
          .panel-title {
              font-size: 34px;
              font-weight: 700;
              text-align: center;
              background: linear-gradient(135deg, #fde6b6, #dba870);
              -webkit-background-clip: text;
              -webkit-text-fill-color: transparent;
              margin-bottom: 8px;
              text-shadow: 0 2px 5px #00000050;
          }
    
          .group-title {
              font-size: 20px;
              font-weight: 600;
              color: #eddabc;
              margin: 8px 0 6px 6px;
              letter-spacing: 1px;
              border-left: 6px solid #e6b567;
              padding-left: 12px;
              text-transform: uppercase;
          }
    
          .button-row {
              display: flex;
              gap: 12px;
              justify-content: center;
              margin-bottom: 8px;
              flex-wrap: wrap;
          }
    
          .btn {
              background: #2f2f3a;
              border: 2px solid #7e6b5a;
              color: #f2e3cf;
              font-size: 18px;
              font-weight: 600;
              padding: 14px 8px;
              border-radius: 60px;
              text-align: center;
              cursor: pointer;
              transition: all 0.15s ease;
              box-shadow: 0 6px 0 #1a1a22, 0 4px 12px black;
              flex: 1 1 0px;
              min-width: 100px;
              backdrop-filter: blur(4px);
              letter-spacing: 0.8px;
              display: flex;
              align-items: center;
              justify-content: center;
              gap: 6px;
          }
    
          .btn.small {
              font-size: 16px;
              padding: 12px 6px;
              min-width: 80px;
          }
    
          .btn.active {
              background: #3c6e8f;
              border-color: #ffcf9a;
              box-shadow: 0 6px 0 #1d404b, 0 4px 12px black;
              color: white;
          }
    
          .btn:hover {
              background: #4a4a5a;
              border-color: #dbb27c;
              transform: translateY(-2px);
              box-shadow: 0 8px 0 #1a1a22, 0 8px 16px black;
          }
    
          .btn:active {
              transform: translateY(4px);
              box-shadow: 0 2px 0 #1a1a22;
          }
    
          /* 方形图标样式 */
          .btn-icon {
              display: inline-flex;
              align-items: center;
              justify-content: center;
              width: 24px;
              height: 24px;
              background: rgba(255, 255, 255, 0.15);
              border: 2px solid #dbb27c;
              border-radius: 6px;
              font-size: 16px;
              font-weight: bold;
              color: #f2e3cf;
              margin-right: 4px;
          }
    
          .btn.small .btn-icon {
              width: 20px;
              height: 20px;
              font-size: 14px;
          }
    
          .status-area {
              font-size: 30px;
              font-weight: 700;
              text-align: center;
              background: #2b2b38cc;
              border-radius: 60px;
              padding: 22px 8px;
              margin: 10px 0 14px 0;
              border: 2px solid #c9a468;
              box-shadow: inset 0 2px 6px #00000060, 0 6px 0 #18181e;
              color: #fff2d4;
              backdrop-filter: blur(4px);
          }
    
          .review-indicator {
              background: #b06e30;
              color: #ffefc0;
              font-weight: bold;
              text-align: center;
              padding: 18px 8px;
              border-radius: 60px;
              font-size: 24px;
              border: 2px solid #ffcf8a;
              box-shadow: 0 6px 0 #5f3e1f;
          }
    
          .file-actions {
              gap: 16px;
          }
    
          #colorPicker {
              position: absolute;
              left: -9999px;
          }
    
          /* 弹窗样式 */
          .victory-modal {
              position: fixed;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
              background: rgba(0, 0, 0, 0.85);
              display: flex;
              justify-content: center;
              align-items: center;
              z-index: 9999;
              backdrop-filter: blur(8px);
              -webkit-backdrop-filter: blur(8px);
          }
    
          .modal-content {
              background: linear-gradient(135deg, #433b32, #2a2620);
              border-radius: 40px;
              padding: 60px 40px;
              text-align: center;
              border: 4px solid #e6b567;
              box-shadow: 0 0 50px rgba(255, 207, 154, 0.6);
              min-width: 400px;
              max-width: 600px;
          }
    
          .modal-title {
              font-size: 48px;
              font-weight: 800;
              margin-bottom: 20px;
              text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
          }
    
          .win-text {
              background: linear-gradient(135deg, #ffdf88, #ffb74d);
              -webkit-background-clip: text;
              -webkit-text-fill-color: transparent;
          }
    
          .lose-text {
              background: linear-gradient(135deg, #ff8a80, #ef5350);
              -webkit-background-clip: text;
              -webkit-text-fill-color: transparent;
          }
    
          .modal-subtitle {
              font-size: 28px;
              color: #f2e3cf;
              margin-bottom: 40px;
          }
    
          .modal-btn {
              background: #2f2f3a;
              border: 3px solid #e6b567;
              color: #f2e3cf;
              font-size: 22px;
              font-weight: 700;
              padding: 18px 40px;
              border-radius: 60px;
              cursor: pointer;
              transition: all 0.2s ease;
              box-shadow: 0 8px 0 #1a1a22, 0 6px 20px black;
          }
    
          .modal-btn:hover {
              background: #4a4a5a;
              border-color: #ffcf9a;
              transform: translateY(-4px);
              box-shadow: 0 12px 0 #1a1a22, 0 8px 25px black;
          }
    
          .modal-btn:active {
              transform: translateY(4px);
              box-shadow: 0 4px 0 #1a1a22;
          }
    
          @media (max-width: 1200px) {
              .game-layout {
                  flex-wrap: wrap;
              }
              .control-panel {
                  width: 320px;
                  max-height: none;
              }
              .modal-content {
                  min-width: 80%;
                  padding: 40px 20px;
              }
              .modal-title {
                  font-size: 36px;
              }
              .modal-subtitle {
                  font-size: 22px;
              }
          }
      </style>
    </head> <body>
    复制代码
      <div class="game-layout">
          <!-- 左侧控制面板 -->
          <div class="control-panel left-panel" id="leftPanel"></div>
    
          <!-- 中央棋盘 (方格放大,边缘留两子宽度) -->
          <div class="board-wrapper">
              <canvas id="boardCanvas" width="900" height="900"></canvas>
          </div>
    
          <!-- 右侧控制面板 -->
          <div class="control-panel right-panel" id="rightPanel"></div>
      </div>
    <script> (function() { // ---------- 常量 & 全局变量 ---------- const BOARD_SIZE = 15; const CELL_SIZE = 46; const BOARD_LEFT = 128; const BOARD_TOP = 128; const PIECE_RADIUS = 20; // 棋盘底色 let BOARD_COLOR = [235, 194, 142]; let bodyBgColor = [44, 62, 79]; let victoryModalShown = false; // 游戏状态类 class Game { constructor() { this.mode = 'pve'; this.difficulty = '中等'; this.board = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0)); this.turn = 1; this.history = []; this.winner = 0; this.aiThinking = false; this.showCoords = true; this.player1Color = [0, 0, 0]; this.player2Color = [255, 255, 255]; this.reviewMode = false; this.reviewStep = -1; this.reviewHistory = []; this.isUndoing = false; // 新增:悔棋状态锁 } reset() { this.board = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0)); this.history = []; this.turn = 1; this.winner = 0; this.reviewMode = false; this.reviewStep = -1; this.reviewHistory = []; this.aiThinking = false; this.isUndoing = false; // 重置悔棋锁 victoryModalShown = false; } placePiece(row, col) { // 悔棋过程中禁止落子 if (this.isUndoing || row<0||row>=BOARD_SIZE||col<0||col>=BOARD_SIZE||this.board[row][col]!==0||this.winner!==0||this.reviewMode) return false; this.board[row][col] = this.turn; this.history.push([row, col, this.turn]); if (this.checkWin(row, col)) { this.winner = this.turn; setTimeout(showVictoryModal, 300); } this.turn = this.turn === 1 ? 2 : 1; return true; } // 核心修复:悔棋逻辑 undo() { // 禁止在复盘/AI思考/无历史记录时悔棋 if (this.history.length === 0 || this.reviewMode || this.aiThinking) return false; // 加悔棋锁,防止AI在悔棋过程中落子 this.isUndoing = true; try { let undoCount = 0; // 人机模式:一次性撤销玩家+AI的两步棋 if (this.mode === 'pve') { // 先撤销最后一步(AI的棋) if (this.history.length > 0) { let [row, col] = this.history.pop(); this.board[row][col] = 0; undoCount++; } // 再撤销玩家的棋(如果有) if (this.history.length > 0) { let [row, col, player] = this.history.pop(); this.board[row][col] = 0; this.turn = player; // 恢复到玩家回合 undoCount++; } } // 人人模式:只撤销一步 else { let [row, col, player] = this.history.pop(); this.board[row][col] = 0; this.turn = player; undoCount++; } // 重置游戏状态 this.winner = 0; victoryModalShown = false; // 移除胜利弹窗 const modal = document.querySelector('.victory-modal'); if (modal) modal.remove(); return undoCount > 0; } finally { // 延迟释放锁,确保状态完全同步 setTimeout(() => { this.isUndoing = false; }, 200); } } checkWin(row, col) { const player = this.board[row][col]; const dirs = [[1,0],[0,1],[1,1],[1,-1]]; for (let [dr, dc] of dirs) { let count = 1; for (let i=1; i<5; i++) { let nr=row+dr*i, nc=col+dc*i; if (nr<0||nr>=BOARD_SIZE||nc<0||nc>=BOARD_SIZE||this.board[nr][nc]!==player) break; count++; } for (let i=1; i<5; i++) { let nr=row-dr*i, nc=col-dc*i; if (nr<0||nr>=BOARD_SIZE||nc<0||nc>=BOARD_SIZE||this.board[nr][nc]!==player) break; count++; } if (count >= 5) return true; } return false; } aiMoveEasy() { let weights = Array(BOARD_SIZE).fill().map(()=>Array(BOARD_SIZE).fill(0)); const dirs = [[1,0],[0,1],[1,1],[1,-1]]; for (let r=0; r<BOARD_SIZE; r++) { for (let c=0; c<BOARD_SIZE; c++) { if (this.board[r][c] !== 0) continue; let score = 0; for (let [dr, dc] of dirs) { let cnt = 1; for (let i=1; i<5; i++) { let rr=r+dr*i, cc=c+dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==2) break; cnt++; } for (let i=1; i<5; i++) { let rr=r-dr*i, cc=c-dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==2) break; cnt++; } if (cnt>=5) score += 10000; else score += cnt*cnt; } for (let [dr, dc] of dirs) { let cnt = 1; for (let i=1; i<5; i++) { let rr=r+dr*i, cc=c+dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==1) break; cnt++; } for (let i=1; i<5; i++) { let rr=r-dr*i, cc=c-dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==1) break; cnt++; } if (cnt>=5) score += 8000; else score += cnt*cnt*0.8; } weights[r][c] = score; } } let maxScore = -1, best = null; for (let r=0; r<BOARD_SIZE; r++) for (let c=0; c<BOARD_SIZE; c++) if (weights[r][c] > maxScore) { maxScore = weights[r][c]; best = [r,c]; } return best; } evaluateMove(r, c, color) { if (this.board[r][c] !== 0) return 0; const dirs = [[1,0],[0,1],[1,1],[1,-1]]; let total = 0; for (let [dr, dc] of dirs) { let cnt1 = 0, cnt2 = 0; for (let i=1; i<5; i++) { let rr=r+dr*i, cc=c+dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==color) break; cnt1++; } for (let i=1; i<5; i++) { let rr=r-dr*i, cc=c-dc*i; if (rr<0||rr>=BOARD_SIZE||cc<0||cc>=BOARD_SIZE||this.board[rr][cc]!==color) break; cnt2++; } let totalLen = 1 + cnt1 + cnt2; if (totalLen >= 5) return 1000000; let leftOpen = true, rightOpen = true; let rr = r + (cnt1+1)*dr, cc = c + (cnt1+1)*dc; if (rr>=0&&rr<BOARD_SIZE&&cc>=0&&cc<BOARD_SIZE && this.board[rr][cc]!==0) rightOpen = false; rr = r - (cnt2+1)*dr; cc = c - (cnt2+1)*dc; if (rr>=0&&rr<BOARD_SIZE&&cc>=0&&cc<BOARD_SIZE && this.board[rr][cc]!==0) leftOpen = false; let live = leftOpen && rightOpen; if (totalLen >= 5) total += 100000; else if (totalLen === 4 && live) total += 10000; else if (totalLen === 4) total += 2000; else if (totalLen === 3 && live) total += 2500; else if (totalLen === 3) total += 400; else total += totalLen * totalLen; } return total; } aiMoveMedium(defenseWeight = 0.8) { let bestScore = -1, bestMove = null; for (let r=0; r<BOARD_SIZE; r++) { for (let c=0; c<BOARD_SIZE; c++) { if (this.board[r][c] !== 0) continue; let attack = this.evaluateMove(r, c, 2); let defense = this.evaluateMove(r, c, 1); let total = attack + defense * defenseWeight; if (total > bestScore) { bestScore = total; bestMove = [r,c]; } } } return bestMove; } saveSgf() { return JSON.stringify({ boardSize: BOARD_SIZE, player1Color: this.player1Color, player2Color: this.player2Color, history: this.history, mode: this.mode, difficulty: this.difficulty }, null, 2); } loadSgf(jsonStr) { try { let data = JSON.parse(jsonStr); if (data.boardSize !== BOARD_SIZE) return false; this.reset(); this.player1Color = data.player1Color; this.player2Color = data.player2Color; this.reviewHistory = data.history || []; this.mode = data.mode || 'pve'; this.difficulty = data.difficulty || '中等'; this.reviewMode = true; this.reviewStep = -1; this.board = Array(BOARD_SIZE).fill().map(()=>Array(BOARD_SIZE).fill(0)); this.turn = 1; this.winner = 0; victoryModalShown = false; return true; } catch (e) { return false; } } reviewForward() { if (!this.reviewMode) return; if (this.reviewStep + 1 < this.reviewHistory.length) { this.reviewStep++; let [row, col, player] = this.reviewHistory[this.reviewStep]; this.board[row][col] = player; this.turn = player === 1 ? 2 : 1; if (this.checkWin(row, col)) this.winner = player; } } reviewBackward() { if (!this.reviewMode || this.reviewStep < 0) return; let [row, col, player] = this.reviewHistory[this.reviewStep]; this.board[row][col] = 0; this.reviewStep--; if (this.reviewStep >= 0) { let [,,lastPlayer] = this.reviewHistory[this.reviewStep]; this.turn = lastPlayer === 1 ? 2 : 1; this.winner = 0; } else { this.turn = 1; this.winner = 0; } victoryModalShown = false; } exitReview() { this.reviewMode = false; this.reviewStep = -1; this.reviewHistory = []; this.reset(); } } const game = new Game(); const canvas = document.getElementById('boardCanvas'); const ctx = canvas.getContext('2d'); const colorPicker = document.getElementById('colorPicker'); // 显示胜利弹窗 function showVictoryModal() { if (victoryModalShown) return; victoryModalShown = true; const modal = document.createElement('div'); modal.className = 'victory-modal'; let isPlayerWin = false; let titleText = ''; let subText = ''; if (game.mode === 'pve') { if (game.winner === 1) { isPlayerWin = true; titleText = '恭喜!'; subText = '你赢了 太牛逼了 🎉'; } else { isPlayerWin = false; titleText = '很遗憾!'; subText = '你输了 小瘪三 😭'; } } else { if (game.winner === 1) { titleText = '黑棋胜利!'; subText = '恭喜黑棋获胜 🎉'; } else { titleText = '白棋胜利!'; subText = '恭喜白棋获胜 🎉'; } } modal.innerHTML = `
    ${titleText}
    ${subText}
    <button class="modal-btn" id="modalRestartBtn">再来一局</button>
    `; document.body.appendChild(modal); document.getElementById('modalRestartBtn').addEventListener('click', () => { game.reset(); drawBoard(); renderPanels(); modal.remove(); }); } // 绘制棋盘 function drawBoard() { ctx.clearRect(0, 0, 900, 900); let boardBgColor = game.winner !== 0 ? '#b0b0b0' : `rgb(${BOARD_COLOR[0]},${BOARD_COLOR[1]},${BOARD_COLOR[2]})`; ctx.fillStyle = boardBgColor; ctx.fillRect(0, 0, 900, 900); ctx.strokeStyle = '#000'; ctx.lineWidth = 2; for (let i=0; i<BOARD_SIZE; i++) { ctx.beginPath(); ctx.moveTo(BOARD_LEFT, BOARD_TOP + i*CELL_SIZE); ctx.lineTo(BOARD_LEFT + (BOARD_SIZE-1)*CELL_SIZE, BOARD_TOP + i*CELL_SIZE); ctx.stroke(); ctx.beginPath(); ctx.moveTo(BOARD_LEFT + i*CELL_SIZE, BOARD_TOP); ctx.lineTo(BOARD_LEFT + i*CELL_SIZE, BOARD_TOP + (BOARD_SIZE-1)*CELL_SIZE); ctx.stroke(); } const stars = [[7,7],[3,3],[11,3],[3,11],[11,11]]; ctx.fillStyle = '#000'; for (let [r,c] of stars) { let x = BOARD_LEFT + c*CELL_SIZE, y = BOARD_TOP + r*CELL_SIZE; ctx.beginPath(); ctx.arc(x, y, 6, 0, 2*Math.PI); ctx.fill(); } for (let r=0; r<BOARD_SIZE; r++) for (let c=0; c<BOARD_SIZE; c++) { if (game.board[r][c] === 0) continue; let x = BOARD_LEFT + c*CELL_SIZE, y = BOARD_TOP + r*CELL_SIZE; let color = game.board[r][c] === 1 ? game.player1Color : game.player2Color; ctx.beginPath(); ctx.arc(x, y, PIECE_RADIUS, 0, 2*Math.PI); ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`; ctx.fill(); ctx.strokeStyle = '#444'; ctx.lineWidth = 2; ctx.stroke(); } if (game.showCoords) { ctx.font = 'bold 20px "Segoe UI", "Microsoft YaHei"'; ctx.fillStyle = '#3a2a1a'; for (let i=0; i<BOARD_SIZE; i++) { ctx.fillText(i+1, BOARD_LEFT-38, BOARD_TOP + i*CELL_SIZE+8); ctx.fillText(String.fromCharCode(65+i), BOARD_LEFT + i*CELL_SIZE-14, BOARD_TOP + (BOARD_SIZE-1)*CELL_SIZE+42); } } if (game.reviewMode) { ctx.font = 'bold 26px "Microsoft YaHei"'; ctx.fillStyle = '#ffb347'; let steps = game.reviewHistory.length; let stepIdx = game.reviewStep+1; let text = `📋 复盘 ${stepIdx}/${steps}`; if (steps === 0) text = '📋 复盘模式'; ctx.fillText(text, BOARD_LEFT, BOARD_TOP-40); } } // 渲染面板 function renderPanels() { const leftDiv = document.getElementById('leftPanel'); const rightDiv = document.getElementById('rightPanel'); if (game.reviewMode) { leftDiv.innerHTML = `
    ⚙️ 复盘
    📌 复盘进行中
    ⏮️ 导航
    上一步
    下一步
    ⏏️ 退出复盘
    `; rightDiv.innerHTML = `
    📁 棋谱信息
    步数: ${game.reviewHistory.length}
    `; document.getElementById('reviewPrev')?.addEventListener('click', ()=>{ game.reviewBackward(); drawBoard(); renderPanels(); }); document.getElementById('reviewNext')?.addEventListener('click', ()=>{ game.reviewForward(); drawBoard(); renderPanels(); }); document.getElementById('reviewExit')?.addEventListener('click', ()=>{ game.exitReview(); drawBoard(); renderPanels(); }); return; } leftDiv.innerHTML = `
    ⚫ 五子棋
    🎮 模式
    🤖 人机
    👥 人人
    📊 难度
    简单
    ★★ 中等
    ★★★ 困难
    ${game.winner!==0?(game.winner===1?'⚫ 黑胜':'⚪ 白胜'):(game.turn===1?'⚫ 黑棋走':'⚪ 白棋走')}
    🕹️ 控制
    重开
    悔棋
    🗺️ 坐标
    `; rightDiv.innerHTML = `
    📋 棋谱
    💾 保存 / 加载
    💾 保存
    📂 加载
    📌 提示
    点击棋盘落子
    🤖 人机自动响应
    `; // 事件绑定 document.getElementById('modePve')?.addEventListener('click', ()=>{ if(game.mode!=='pve'){game.mode='pve'; game.reset(); drawBoard(); renderPanels();}}); document.getElementById('modePvp')?.addEventListener('click', ()=>{ if(game.mode!=='pvp'){game.mode='pvp'; game.reset(); drawBoard(); renderPanels();}}); for (let i=0; i<3; i++) { document.getElementById(`diff${i}`)?.addEventListener('click', ()=>{ game.difficulty=['简单','中等','困难'][i]; renderPanels(); }); } document.getElementById('resetBtn')?.addEventListener('click', ()=>{ game.reset(); const modal = document.querySelector('.victory-modal'); if (modal) modal.remove(); drawBoard(); renderPanels(); }); // 修复悔棋按钮:增加失败提示 document.getElementById('undoBtn')?.addEventListener('click', ()=>{ if(game.undo()) { drawBoard(); renderPanels(); } else { alert('无法悔棋!(无历史记录/复盘模式/AI思考中)'); } }); document.getElementById('toggleCoords')?.addEventListener('click', ()=>{ game.showCoords = !game.showCoords; drawBoard(); renderPanels(); }); document.getElementById('saveBtn')?.addEventListener('click', ()=>{ if(game.history.length) { const json=game.saveSgf(); const blob=new Blob([json]); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='gobang.json'; a.click(); } else alert('无棋谱'); }); document.getElementById('loadBtn')?.addEventListener('click', ()=>{ const input = document.createElement('input'); input.type='file'; input.accept='.json'; input.onchange = (e) => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (ev) => { if(game.loadSgf(ev.target.result)) { drawBoard(); renderPanels(); } else alert('加载失败'); }; reader.readAsText(file); }; input.click(); }); } // 鼠标落子 canvas.addEventListener('click', (e)=>{ // 悔棋过程中禁止落子 if (game.isUndoing) return; const rect = canvas.getBoundingClientRect(); const scale = canvas.width / rect.width; const mouseX = (e.clientX - rect.left) * scale; const mouseY = (e.clientY - rect.top) * scale; if (mouseX >= BOARD_LEFT && mouseX <= BOARD_LEFT+(BOARD_SIZE-1)*CELL_SIZE && mouseY >= BOARD_TOP && mouseY <= BOARD_TOP+(BOARD_SIZE-1)*CELL_SIZE) { let col = Math.round((mouseX - BOARD_LEFT)/CELL_SIZE); let row = Math.round((mouseY - BOARD_TOP)/CELL_SIZE); row = Math.max(0, Math.min(BOARD_SIZE-1, row)); col = Math.max(0, Math.min(BOARD_SIZE-1, col)); if (!game.reviewMode) { let isPlaced = false; if (game.mode==='pve' && game.turn===1 && game.winner===0) { isPlaced = game.placePiece(row,col); } else if (game.mode==='pvp' && game.winner===0) { isPlaced = game.placePiece(row,col); } // 只有落子成功才刷新界面 if (isPlaced) { drawBoard(); renderPanels(); } } } }); // 修复AI落子逻辑:增加悔棋锁检查 function aiTurn() { // 悔棋中/复盘/非人机模式/非AI回合/游戏结束/AI思考中 都不执行 if (game.isUndoing || game.reviewMode || game.mode !== 'pve' || game.turn !== 2 || game.winner !== 0 || game.aiThinking) { return; } game.aiThinking = true; // 增加AI思考延迟(从30ms改为300ms),提升体验 setTimeout(() => { let best = game.difficulty === '简单' ? game.aiMoveEasy() : game.aiMoveMedium(game.difficulty === '中等' ? 0.8 : 0.9); if (best) game.placePiece(best[0], best[1]); game.aiThinking = false; drawBoard(); renderPanels(); }, 300); } function loop() { drawBoard(); aiTurn(); requestAnimationFrame(loop); } document.body.style.backgroundColor = `rgb(${bodyBgColor[0]},${bodyBgColor[1]},${bodyBgColor[2]})`; renderPanels(); loop(); })(); </script> </body> </html>
相关推荐
BUG创建者1 小时前
openlayers上跟据经纬度画出轨迹
开发语言·javascript·vue·html
IT_陈寒2 小时前
SpringBoot项目启动速度提升300%?这5个隐藏配置太关键了!
前端·人工智能·后端
小碗细面2 小时前
5 分钟上手 Claude 自定义 Subagents
前端·人工智能·ai编程
漫随流水2 小时前
HTML和CSS和JavaScript的区别
javascript·css·html
小J听不清2 小时前
CSS 浮动(float)全解析:布局 / 文字环绕 / 清除浮动
前端·javascript·css·html·css3
wuhen_n2 小时前
生产环境极致优化:拆包、图片压缩、Gzip/Brotli 完全指南
前端·javascript·vue.js
用户69371750013842 小时前
315曝光AI搜索问题:GEO技术靠内容投喂操控答案,新型营销操作全揭秘
android·前端·人工智能
周星星日记2 小时前
pnpm为什么成为"最先进的管理包工具"
前端
多厘2 小时前
使用 opencode 和灵感写一个 mac App (实操版)
前端·github