围棋-html版本

围棋-html版本 一、界面布局软件采用左右两栏布局左侧为棋盘及信息区右侧为垂直排列的功能按钮整体风格古典雅致。棋盘区19×19 标准棋盘采用立体感棋子、木质底色并带有星标天元、小目等。棋盘支持点击落子并有落点偏离提示。计时器上方显示黑方#9899;与白方#9898;的剩余时间默认每方 30 分钟可修改源码中的 INIT_TIME 常量。当前行棋方的计时器会有高亮边框。统计栏下方显示黑方提子数、白方提子数以及当前落子手数。按钮组右侧竖向排列八个功能按钮涵盖对局控制、棋谱管理、界面个性化等。二、核心对局功能1. 落子规则点击棋盘交叉点即可落子程序自动检测该位置是否为空。提子落子后自动移除被包围且无气的敌方棋子并累加提子计数。禁着点检测自杀禁止落子后若己方棋子无气则判为非法。劫争检测禁止立即重复上一回合的全局局面即“劫”。落子合法后切换玩家并启动对应方计时器。2. 虚一手Pass点击“虚一手”按钮表示当前玩家放弃落子轮由对方行棋。虚手也会记录在历史中便于连续虚手后的协商终局。3. 悔棋点击“悔棋”可逐步回退至上一手棋前的状态包括提子数、手数、计时器均回退。程序内部维护完整的历史栈确保悔棋逻辑准确。4. 认输点击“认输”立即结束对局弹出胜负提示计时停止棋盘锁定。5. 新局重置所有状态棋盘清空、计时重置为 30 分钟、提子与手数归零、历史清空并由黑方先手开始计时。三、计时规则采用倒计时制每方独立计时当前行棋方计时递减。计时器每秒更新一次时间耗尽时自动判负超时方输。切换玩家时计时器自动切换游戏结束后计时停止。四、棋谱管理1. 导出棋谱点击“导出棋谱”可将当前对局的落子序列保存为 JSON 文件。文件格式如下json复制下载{ format: qingstone-go, boardSize: 19, moves: [ { color: B, row: 3, col: 16 }, { color: W, pass: true }, ... ]}每步棋记录颜色B/W、坐标row, col或虚手pass: true。文件名自动包含时间戳。2. 导入棋谱点击“导入棋谱”选择本地 JSON 文件程序会按顺序自动落子并实时校验每一步的合法性如颜色顺序、禁着点等。若棋谱非法会提示错误并重置棋盘。五、个性化设置1. 棋盘颜色点击“棋盘颜色”按钮通过颜色选择器修改棋盘底色。线条与星标的颜色会根据底色自动加深保持视觉对比度实现一键换肤。2. 背景颜色点击“背景颜色”按钮可修改页面背景的径向渐变主色程序自动生成对应的深色渐变营造不同的对局氛围。六、技术特色纯前端实现HTML CSS JavaScript无任何外部依赖可离线运行。精确的围棋规则实现了连通块气数计算、提子、劫争、自杀检测等核心算法。历史与动作双记录既支持悔棋所需的完整状态快照history也保留了轻量的动作序列moveHistory用于导入导出。视觉细节棋子使用径向渐变模拟立体感棋盘线条粗细适中点击时有轻微反馈亮度变化。响应式布局棋盘尺寸基于 Canvas 固定 900×900 像素但通过 CSS 限制最大宽度在不同屏幕下均可正常显示。!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title围棋 · 清石/title style * { box-sizing: border-box; user-select: none; } body { background: #2b5d3b; background: radial-gradient(circle at 20% 30%, #3f8654, #1e4a2f); min-height: 100vh; margin: 0; display: flex; justify-content: center; align-items: center; font-family: Segoe UI, Roboto, Helvetica Neue, sans-serif; padding: 20px; transition: background 0.2s; } .go-container { background: transparent; padding: 0; border: none; box-shadow: none; width: fit-content; margin: 0 auto; } .main-layout { display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: center; } .board-area { display: flex; flex-direction: column; align-items: center; } canvas { display: block; width: 100%; height: auto; max-width: 900px; aspect-ratio: 1 / 1; border-radius: 24px; background: #e5c8a3; box-shadow: inset 0 0 0 2px #9b7e5f, 0 20px 25px rgba(0,0,0,0.6); cursor: pointer; transition: filter 0.1s; } canvas:active { filter: brightness(0.97); } .timer-row { display: flex; justify-content: space-between; gap: 30px; width: 100%; margin-bottom: 15px; font-size: 1.4rem; font-weight: 600; color: #2d1f13; } .timer { display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 4px 12px; border-radius: 40px; transition: all 0.2s; } .timer.black-timer-active { outline: 3px solid #ffd966; background: rgba(255, 217, 102, 0.15); } .timer.white-timer-active { outline: 3px solid #ffd966; background: rgba(255, 217, 102, 0.15); } .timer span { background: #f0e0d0; padding: 4px 15px; border-radius: 40px; color: #3d2b1b; font-size: 1.3rem; font-weight: 600; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } .stats { display: flex; gap: 25px; font-size: 1.3rem; color: #2d1f13; text-shadow: 0 1px 0 #eeddbb; margin-top: 15px; } .stats div { display: flex; align-items: center; gap: 8px; white-space: nowrap; } .stats span { background: #f0e0d0; padding: 4px 12px; border-radius: 30px; font-weight: 700; color: #3d2b1b; min-width: 45px; text-align: center; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } .button-group-vertical { display: flex; flex-direction: column; gap: 8px; min-width: 90px; /* 稍微加宽以适应中文 */ } .go-button { background: #efe0c9; border: none; padding: 6px 0; font-size: 0.9rem; font-weight: bold; border-radius: 30px; color: #3d2b1b; box-shadow: 0 3px 0 #7a5f45, 0 4px 6px black; cursor: pointer; transition: 0.07s linear; border: 1px solid #ffefd1; letter-spacing: 0.5px; width: 100%; text-align: center; white-space: normal; line-height: 1.2; word-break: keep-all; } .go-button:hover { background: #f5ead7; } .go-button:active { transform: translateY(3px); box-shadow: 0 1px 0 #7a5f45, 0 4px 6px black; } /* 隐藏的原生文件上传按钮 颜色选择器 */ #importFileInput, #boardColorPicker, #bgColorPicker { display: none; } /style /head body div classgo-container div classmain-layout div classboard-area div classtimer-row div classtimer idblackTimerDisplay#9899; span30:00/span/div div classtimer idwhiteTimerDisplay#9898; span30:00/span/div /div canvas idgoBoard width900 height900/canvas div classstats div#9899; span idblackCaptures0/span/div div#9898; span idwhiteCaptures0/span/div div#128070;span idmoveCount0/span/div /div /div div classbutton-group-vertical button classgo-button idpassBtn虚一手/button button classgo-button idundoBtn悔棋/button button classgo-button idresetBtn新局/button button classgo-button idresignBtn认输/button button classgo-button idexportBtn导出棋谱/button button classgo-button idimportBtn导入棋谱/button !-- 新增两个自定义颜色按钮 -- button classgo-button idboardColorBtn棋盘颜色/button button classgo-button idbgColorBtn背景颜色/button /div /div /div !-- 隐藏的file input 颜色选择器 -- input typefile idimportFileInput accept.json,application/json input typecolor idboardColorPicker value#e5c8a3 input typecolor idbgColorPicker value#2b5d3b script (function(){ // ----- 常量 ----- const BOARD_SIZE 19; const EMPTY 0; const BLACK 1; const WHITE 2; const MARGIN 55; const CANVAS_SIZE 900; const INIT_TIME 1800; // ----- 全局状态 ----- let board []; let currentPlayer BLACK; let prevBoard []; // 用于劫检测 let gameOver false; // 统计 let blackCaptures 0; let whiteCaptures 0; let moveCount 0; // 计时器相关 let blackTime INIT_TIME; let whiteTime INIT_TIME; let timerInterval null; // 历史记录存储每一步之后的状态 { board, blackCaptures, whiteCaptures, moveCount, currentPlayer } let history []; // 落子动作序列 (用于导入/导出棋谱) let moveHistory []; // 每个元素: { color: BLACK/WHITE, row, col } 或 { color: BLACK/WHITE, pass: true } // ---------- 新增自定义颜色变量 ---------- let boardBgColor #e5c8a3; // 棋盘底色 let boardLineColor #5d3f28; // 线条、星标颜色 (默认深棕) // DOM 元素 const canvas document.getElementById(goBoard); const ctx canvas.getContext(2d); const blackCapturesSpan document.getElementById(blackCaptures); const whiteCapturesSpan document.getElementById(whiteCaptures); const moveCountSpan document.getElementById(moveCount); const blackTimerDisplay document.getElementById(blackTimerDisplay); const whiteTimerDisplay document.getElementById(whiteTimerDisplay); // 新增导入文件输入 颜色选择器 const importFileInput document.getElementById(importFileInput); const boardColorPicker document.getElementById(boardColorPicker); const bgColorPicker document.getElementById(bgColorPicker); // 提示函数 function setMessage(msg) { alert(msg); } // ----- 辅助函数 ----- function copyBoard(src) { return src.map(row [...row]); } function boardsEqual(b1, b2) { for (let i 0; i BOARD_SIZE; i) { for (let j 0; j BOARD_SIZE; j) { if (b1[i][j] ! b2[i][j]) return false; } } return true; } // ----- 获取连通块信息 ----- function getGroupInfo(boardState, row, col, color) { if (boardState[row][col] ! color) return { points: [], libertyCount: 0 }; const visited Array(BOARD_SIZE).fill().map(() Array(BOARD_SIZE).fill(false)); const queue [[row, col]]; visited[row][col] true; const points []; const libertySet new Set(); while (queue.length) { const [r, c] queue.shift(); points.push([r, c]); const dirs [[-1, 0], [1, 0], [0, -1], [0, 1]]; for (let [dr, dc] of dirs) { const nr r dr, nc c dc; if (nr 0 nr BOARD_SIZE nc 0 nc BOARD_SIZE) { if (boardState[nr][nc] EMPTY) { libertySet.add(${nr},${nc}); } else if (boardState[nr][nc] color !visited[nr][nc]) { visited[nr][nc] true; queue.push([nr, nc]); } } } } return { points, libertyCount: libertySet.size }; } // 移除无气棋子并返回移除数量 function removeDeadGroups(boardState, color) { const toRemove []; const visited Array(BOARD_SIZE).fill().map(() Array(BOARD_SIZE).fill(false)); for (let r 0; r BOARD_SIZE; r) { for (let c 0; c BOARD_SIZE; c) { if (boardState[r][c] color !visited[r][c]) { const { points, libertyCount } getGroupInfo(boardState, r, c, color); for (let [pr, pc] of points) { visited[pr][pc] true; } if (libertyCount 0) { toRemove.push(...points); } } } } for (let [r, c] of toRemove) { boardState[r][c] EMPTY; } return toRemove.length; } // 检查自杀 function hasSelfDestruct(boardState, color) { const visited Array(BOARD_SIZE).fill().map(() Array(BOARD_SIZE).fill(false)); for (let r 0; r BOARD_SIZE; r) { for (let c 0; c BOARD_SIZE; c) { if (boardState[r][c] color !visited[r][c]) { const { points, libertyCount } getGroupInfo(boardState, r, c, color); for (let [pr, pc] of points) visited[pr][pc] true; if (libertyCount 0) return true; } } } return false; } // ----- 计时器函数 ----- function formatTime(seconds) { if (seconds 0) seconds 0; const mins Math.floor(seconds / 60); const secs seconds % 60; return ${mins.toString().padStart(2, 0)}:${secs.toString().padStart(2, 0)}; } function updateTimerDisplay() { blackTimerDisplay.innerHTML #9899; span${formatTime(blackTime)}/span; whiteTimerDisplay.innerHTML #9898; span${formatTime(whiteTime)}/span; if (!gameOver) { if (currentPlayer BLACK) { blackTimerDisplay.classList.add(black-timer-active); whiteTimerDisplay.classList.remove(white-timer-active); } else { whiteTimerDisplay.classList.add(white-timer-active); blackTimerDisplay.classList.remove(black-timer-active); } } else { blackTimerDisplay.classList.remove(black-timer-active); whiteTimerDisplay.classList.remove(white-timer-active); } } function stopTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval null; } } function timeLoss(player) { if (gameOver) return; gameOver true; stopTimer(); const loser (player BLACK) ? 黑棋 : 白棋; const winner (player BLACK) ? 白棋 : 黑棋; alert(#9200; ${loser} 超时 · ${winner} 获胜); updateTimerDisplay(); drawBoard(); } function startTimer(player) { if (gameOver) return; stopTimer(); timerInterval setInterval(() { if (gameOver) { stopTimer(); return; } if (currentPlayer BLACK) { blackTime--; if (blackTime 0) { blackTime 0; timeLoss(BLACK); } } else { whiteTime--; if (whiteTime 0) { whiteTime 0; timeLoss(WHITE); } } updateTimerDisplay(); }, 1000); } // 切换玩家 function switchPlayerAndTimer(newPlayer) { currentPlayer newPlayer; stopTimer(); if (!gameOver) { startTimer(currentPlayer); } updateStats(); updateTimerDisplay(); } // 保存当前状态到历史 (落子后调用) function pushHistory() { history.push({ board: copyBoard(board), blackCaptures: blackCaptures, whiteCaptures: whiteCaptures, moveCount: moveCount, currentPlayer: currentPlayer }); } // 从历史恢复状态 (用于悔棋) function restoreFromHistory(index) { const state history[index]; board copyBoard(state.board); blackCaptures state.blackCaptures; whiteCaptures state.whiteCaptures; moveCount state.moveCount; currentPlayer state.currentPlayer; if (index 0) { prevBoard copyBoard(history[index-1].board); } else { prevBoard Array(BOARD_SIZE).fill().map(() Array(BOARD_SIZE).fill(EMPTY)); } gameOver false; stopTimer(); startTimer(currentPlayer); updateStats(); updateTimerDisplay(); drawBoard(); } // ----- 落子逻辑 ----- function tryMove(row, col) { if (gameOver) { alert(#127937; 游戏已结束请按【新局】); return false; } if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) return false; if (board[row][col] ! EMPTY) { alert(#10060; 此处已有棋子); return false; } const opponent currentPlayer BLACK ? WHITE : BLACK; const newBoard copyBoard(board); newBoard[row][col] currentPlayer; const captured removeDeadGroups(newBoard, opponent); if (hasSelfDestruct(newBoard, currentPlayer)) { alert(#9940; 自杀禁止); return false; } if (boardsEqual(newBoard, prevBoard)) { alert(#128260; 劫争 — 不能立即重复局面); return false; } if (currentPlayer BLACK) { blackCaptures captured; } else { whiteCaptures captured; } prevBoard copyBoard(board); board newBoard; moveCount; pushHistory(); // 保存新状态到历史 // 记录动作到 moveHistory moveHistory.push({ color: currentPlayer, row: row, col: col }); const nextPlayer opponent; switchPlayerAndTimer(nextPlayer); updateStats(); drawBoard(); return true; } // ----- 虚一手 ----- function pass() { if (gameOver) { alert(游戏已结束请按新局); return; } const nextPlayer (currentPlayer BLACK) ? WHITE : BLACK; prevBoard copyBoard(board); pushHistory(); // 虚手也视为一步历史 (棋盘不变) moveHistory.push({ color: currentPlayer, pass: true }); switchPlayerAndTimer(nextPlayer); drawBoard(); } // ----- 悔棋 (同步moveHistory) ----- function undo() { if (gameOver) { alert(游戏已结束无法悔棋); return; } if (history.length 2) { alert(无法继续悔棋); return; } history.pop(); if (moveHistory.length 0) { moveHistory.pop(); } const lastIndex history.length - 1; restoreFromHistory(lastIndex); } // ----- 认输 ----- function resign() { if (gameOver) return; gameOver true; stopTimer(); const loser (currentPlayer BLACK) ? 黑棋 : 白棋; const winner (currentPlayer BLACK) ? 白棋 : 黑棋; alert(#127987;#65039; ${loser} 认输 · ${winner} 获胜); updateTimerDisplay(); drawBoard(); } function resetGame() { stopTimer(); board Array(BOARD_SIZE).fill().map(() Array(BOARD_SIZE).fill(EMPTY)); prevBoard copyBoard(board); currentPlayer BLACK; gameOver false; blackCaptures 0; whiteCaptures 0; moveCount 0; blackTime INIT_TIME; whiteTime INIT_TIME; history []; moveHistory []; pushHistory(); // 初始空棋盘状态 updateStats(); drawBoard(); updateTimerDisplay(); startTimer(BLACK); } // ----- 绘制棋盘 (使用自定义颜色) ----- function drawBoard() { ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); // 使用自定义棋盘底色 ctx.fillStyle boardBgColor; ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); const step (CANVAS_SIZE - 2 * MARGIN) / (BOARD_SIZE - 1); ctx.lineWidth 2.2; ctx.strokeStyle boardLineColor; // 线条颜色 for (let i 0; i BOARD_SIZE; i) { const x MARGIN i * step; ctx.beginPath(); ctx.moveTo(x, MARGIN); ctx.lineTo(x, CANVAS_SIZE - MARGIN); ctx.stroke(); const y MARGIN i * step; ctx.beginPath(); ctx.moveTo(MARGIN, y); ctx.lineTo(CANVAS_SIZE - MARGIN, y); ctx.stroke(); } const stars [3, 9, 15]; ctx.fillStyle boardLineColor; // 星标颜色与线条一致 for (let r of stars) { for (let c of stars) { const x MARGIN c * step; const y MARGIN r * step; ctx.beginPath(); ctx.arc(x, y, step * 0.25, 0, 2 * Math.PI); ctx.fill(); } } for (let r 0; r BOARD_SIZE; r) { for (let c 0; c BOARD_SIZE; c) { if (board[r][c] EMPTY) continue; const x MARGIN c * step; const y MARGIN r * step; const radius step * 0.44; ctx.shadowColor rgba(0,0,0,0.6); ctx.shadowBlur 12; ctx.shadowOffsetX 4; ctx.shadowOffsetY 4; if (board[r][c] BLACK) { const gradient ctx.createRadialGradient(x-6, y-6, radius*0.2, x, y, radius*1.5); gradient.addColorStop(0, #333); gradient.addColorStop(0.7, #111); gradient.addColorStop(1, #000); ctx.fillStyle gradient; } else { const gradient ctx.createRadialGradient(x-6, y-6, radius*0.3, x, y, radius*1.5); gradient.addColorStop(0, #fefefe); gradient.addColorStop(0.6, #dddddd); gradient.addColorStop(1, #aaaaaa); ctx.fillStyle gradient; } ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.fill(); ctx.shadowBlur 6; ctx.shadowOffsetX 2; ctx.shadowOffsetY 2; ctx.strokeStyle board[r][c] BLACK ? #2f2f2f : #f0f0f0; ctx.lineWidth 2.2; ctx.stroke(); } } ctx.shadowColor transparent; ctx.shadowBlur 0; ctx.shadowOffsetX 0; ctx.shadowOffsetY 0; } function updateStats() { blackCapturesSpan.innerText blackCaptures; whiteCapturesSpan.innerText whiteCaptures; moveCountSpan.innerText moveCount; } // ----- 鼠标点击处理 ----- function handleCanvasClick(e) { const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; const mouseX (e.clientX - rect.left) * scaleX; const mouseY (e.clientY - rect.top) * scaleY; const step (CANVAS_SIZE - 2 * MARGIN) / (BOARD_SIZE - 1); const gridCol Math.round((mouseX - MARGIN) / step); const gridRow Math.round((mouseY - MARGIN) / step); if (gridRow 0 gridRow BOARD_SIZE gridCol 0 gridCol BOARD_SIZE) { const crossX MARGIN gridCol * step; const crossY MARGIN gridRow * step; const dist Math.hypot(mouseX - crossX, mouseY - crossY); if (dist step * 0.6) { tryMove(gridRow, gridCol); } else { alert(#9940; 点击位置偏离交叉点); } } else { alert(#9940; 棋盘外); } } // ---------- 导出棋谱 ---------- function exportGame() { if (moveHistory.length 0) { alert(没有落子记录无法导出空棋谱); return; } const exportMoves moveHistory.map(m { if (m.pass) { return { color: m.color BLACK ? B : W, pass: true }; } else { return { color: m.color BLACK ? B : W, row: m.row, col: m.col }; } }); const gameData { format: qingstone-go, boardSize: BOARD_SIZE, moves: exportMoves }; const jsonStr JSON.stringify(gameData, null, 2); const blob new Blob([jsonStr], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 围棋棋谱_${new Date().toISOString().slice(0,19).replace(/:/g, -)}.json; a.click(); URL.revokeObjectURL(url); } // ---------- 导入棋谱 ---------- function importGameFromFile(file) { const reader new FileReader(); reader.onload (e) { try { const content e.target.result; const gameData JSON.parse(content); if (!gameData.moves || !Array.isArray(gameData.moves) || (gameData.boardSize gameData.boardSize ! BOARD_SIZE)) { throw new Error(棋谱格式不符或棋盘大小不为19); } stopTimer(); resetGame(); stopTimer(); const originalAlert window.alert; window.alert function(){}; for (const m of gameData.moves) { const color m.color B ? BLACK : WHITE; if (currentPlayer ! color) { throw new Error(棋谱顺序错误期待${currentPlayerBLACK?黑:白}但动作是${m.color}); } if (m.pass) { pass(); } else { if (m.row undefined || m.col undefined) throw new Error(缺少坐标); const success tryMove(m.row, m.col); if (!success) throw new Error(落子 (${m.row},${m.col}) 非法); } } window.alert originalAlert; startTimer(currentPlayer); updateTimerDisplay(); drawBoard(); alert(#9989; 棋谱导入成功); } catch (err) { window.alert originalAlert || alert; alert(#10060; 导入失败 err.message); resetGame(); } finally { importFileInput.value ; } }; reader.readAsText(file); } // 导入按钮触发隐藏file input function onImportClick() { importFileInput.click(); } // ---------- 颜色工具函数hex变暗 ---------- function darkenColor(hex, factor) { // 去除 #解析rgb let r parseInt(hex.slice(1,3), 16); let g parseInt(hex.slice(3,5), 16); let b parseInt(hex.slice(5,7), 16); r Math.min(255, Math.max(0, Math.floor(r * factor))); g Math.min(255, Math.max(0, Math.floor(g * factor))); b Math.min(255, Math.max(0, Math.floor(b * factor))); return #${((1 24) (r 16) (g 8) b).toString(16).slice(1)}; } // 设置背景渐变 (基于选中的底色) function setBodyGradient(baseColor) { const dark darkenColor(baseColor, 0.5); // 变暗作为渐变终点 document.body.style.background radial-gradient(circle at 20% 30%, ${baseColor}, ${dark}); } // 监听文件选择 importFileInput.addEventListener(change, (e) { const file e.target.files[0]; if (file) { importGameFromFile(file); } }); // ----- 新增颜色自定义逻辑 ----- // 棋盘颜色按钮触发颜色选择器 document.getElementById(boardColorBtn).addEventListener(click, () { boardColorPicker.click(); }); // 棋盘颜色选择变化 boardColorPicker.addEventListener(change, (e) { const newBase e.target.value; boardBgColor newBase; // 线条颜色自动变暗保持对比 (因子0.45 接近原始对比度) boardLineColor darkenColor(newBase, 0.4); drawBoard(); // 重绘棋盘 }); // 背景颜色按钮 document.getElementById(bgColorBtn).addEventListener(click, () { bgColorPicker.click(); }); bgColorPicker.addEventListener(change, (e) { const newBg e.target.value; setBodyGradient(newBg); }); // ----- 事件绑定 ----- canvas.addEventListener(click, handleCanvasClick); document.getElementById(passBtn).addEventListener(click, pass); document.getElementById(undoBtn).addEventListener(click, undo); document.getElementById(resignBtn).addEventListener(click, resign); document.getElementById(resetBtn).addEventListener(click, resetGame); document.getElementById(exportBtn).addEventListener(click, exportGame); document.getElementById(importBtn).addEventListener(click, onImportClick); // 启动游戏 resetGame(); // 初始化背景渐变 (使用默认颜色) setBodyGradient(bgColorPicker.value); // 如果希望初始线条也由底色自动生成可以打开下面注释: // boardLineColor darkenColor(boardColorPicker.value, 0.4); // drawBoard(); })(); /script /body /html