好友
阅读权限10
听众
最后登录1970-1-1
|
围棋专业版 html代码 基于 HTML5 Canvas 开发的本地双人对弈围棋程序,界面精致、规则严谨,并提供丰富的对局辅助功能。它完全运行于浏览器中,无需安装,打开即可使用,适合棋友面对面切磋、棋谱记录与复盘。
一、界面布局软件采用左右两栏布局,左侧为棋盘及信息区,右侧为垂直排列的功能按钮,整体风格古典雅致。- 棋盘区:19×19 标准棋盘,采用立体感棋子、木质底色,并带有星标(天元、小目等)。棋盘支持点击落子,并有落点偏离提示。
- 计时器:上方显示黑方(⚫)与白方(⚪)的剩余时间,默认每方 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 限制最大宽度,在不同屏幕下均可正常显示。
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.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 class="go-container">
<div class="main-layout">
<div class="board-area">
<div class="timer-row">
<div class="timer" id="blackTimerDisplay">⚫ <span>30:00</span></div>
<div class="timer" id="whiteTimerDisplay">⚪ <span>30:00</span></div>
</div>
<canvas id="goBoard" width="900" height="900"></canvas>
<div class="stats">
<div>⚫ <span id="blackCaptures">0</span></div>
<div>⚪ <span id="whiteCaptures">0</span></div>
<div>👆<span id="moveCount">0</span></div>
</div>
</div>
<div class="button-group-vertical">
<button class="go-button" id="passBtn">虚一手</button>
<button class="go-button" id="undoBtn">悔棋</button>
<button class="go-button" id="resetBtn">新局</button>
<button class="go-button" id="resignBtn">认输</button>
<button class="go-button" id="exportBtn">导出棋谱</button>
<button class="go-button" id="importBtn">导入棋谱</button>
<!-- 新增两个自定义颜色按钮 -->
<button class="go-button" id="boardColorBtn">棋盘颜色</button>
<button class="go-button" id="bgColorBtn">背景颜色</button>
</div>
</div>
</div>
<!-- 隐藏的file input & 颜色选择器 -->
<input type="file" id="importFileInput" accept=".json,application/json">
<input type="color" id="boardColorPicker" value="#e5c8a3">
<input type="color" id="bgColorPicker" 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 = `⚫ <span>${formatTime(blackTime)}</span>`;
whiteTimerDisplay.innerHTML = `⚪ <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(`⏰ ${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('🏁 游戏已结束,请按【新局】');
return false;
}
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return false;
if (board[row][col] !== EMPTY) {
alert('❌ 此处已有棋子');
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('⛔ 自杀禁止');
return false;
}
if (boardsEqual(newBoard, prevBoard)) {
alert('🔄 劫争 — 不能立即重复局面');
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(`🏳️ ${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('⛔ 点击位置偏离交叉点');
}
} else {
alert('⛔ 棋盘外');
}
}
// ---------- 导出棋谱 ----------
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(`棋谱顺序错误:期待${currentPlayer===BLACK?'黑':'白'},但动作是${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('✅ 棋谱导入成功');
} catch (err) {
window.alert = originalAlert || alert;
alert('❌ 导入失败:' + 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>
|
-
围棋截屏
-
-
围棋专业版.rar
7.25 KB, 下载次数: 0, 下载积分: 吾爱币 -1 CB
围棋软件专业版 html原码软件
|