吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2|回复: 0
收起左侧

[其他原创] 围棋专业版 - html代码

[复制链接]
xhd335 发表于 2026-3-18 18:56
围棋专业版 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">&#9899; <span>30:00</span></div>
                    <div class="timer" id="whiteTimerDisplay">&#9898; <span>30:00</span></div>
                </div>
                <canvas id="goBoard" width="900" height="900"></canvas>
                <div class="stats">
                    <div>&#9899; <span id="blackCaptures">0</span></div>
                    <div>&#9898; <span id="whiteCaptures">0</span></div>
                    <div>&#128070;<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 = `&#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(`棋谱顺序错误:期待${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('&#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>

围棋截屏

围棋截屏

围棋专业版.rar

7.25 KB, 下载次数: 0, 下载积分: 吾爱币 -1 CB

围棋软件专业版 html原码软件

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-3-18 18:56

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表