deepseek v4象棋编写测试

deepseek v4编写的AI人机对弈象棋,进步相当大。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>中国象棋 - 人机对弈</title>
    <style>
        :root {
            --bg: #f0e6d3;
            --board-bg: #e8d5b0;
            --board-border: #5d3a1a;
            --text: #3a1f04;
            --red: #c41e3a;
            --red-dark: #8b0000;
            --black: #1a1a1a;
            --gold: #c8960c;
            --highlight: #ffe484;
            --ai-glow: #4da6ff;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #3a2f28 0%, #5a3d2b 30%, #4a3020 60%, #2a1f15 100%);
            background-attachment: fixed;
            font-family: 'STSong', 'Songti SC', 'Noto Serif SC', 'SimSun', 'KaiTi', 'STKaiti', '楷体', '宋体', serif;
            user-select: none;
            -webkit-user-select: none;
            -webkit-tap-highlight-color: transparent;
            padding: 10px;
        }

        .game-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 16px;
            background: rgba(255, 255, 255, 0.03);
            border-radius: 20px;
            padding: 20px 24px 24px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
            backdrop-filter: blur(2px);
        }

        .game-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            width: 100%;
            max-width: 600px;
            gap: 16px;
            flex-wrap: wrap;
        }

        .game-title {
            font-size: 1.6em;
            font-weight: bold;
            color: #f0d9a0;
            letter-spacing: 0.08em;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
            white-space: nowrap;
        }

        .ai-badge {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            font-size: 0.7em;
            background: linear-gradient(135deg, #1a3a5c, #0d2137);
            color: #7ec8f8;
            padding: 4px 10px;
            border-radius: 14px;
            letter-spacing: 0.06em;
            border: 1px solid #3a6a9a;
            animation: ai-pulse 2.5s infinite;
        }
        .ai-badge .ai-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #4da6ff;
            box-shadow: 0 0 8px #4da6ff;
            animation: ai-dot-blink 1.2s infinite;
        }
        @keyframes ai-pulse {
            0%,
            100% {
                box-shadow: 0 0 8px rgba(77, 166, 255, 0.2);
            }
            50% {
                box-shadow: 0 0 18px rgba(77, 166, 255, 0.5);
            }
        }
        @keyframes ai-dot-blink {
            0%,
            100% {
                opacity: 1;
            }
            50% {
                opacity: 0.3;
            }
        }

        .turn-indicator {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 16px;
            border-radius: 25px;
            font-size: 1em;
            font-weight: bold;
            letter-spacing: 0.05em;
            transition: all 0.3s ease;
            background: rgba(0, 0, 0, 0.3);
            color: #ddd;
            border: 2px solid transparent;
            white-space: nowrap;
        }
        .turn-indicator.red-turn {
            background: rgba(200, 30, 50, 0.2);
            border-color: #c41e3a;
            color: #ffaaaa;
            box-shadow: 0 0 20px rgba(200, 30, 50, 0.3);
            animation: pulse-red 2s infinite;
        }
        .turn-indicator.black-turn {
            background: rgba(30, 50, 80, 0.5);
            border-color: #4da6ff;
            color: #b8d8f8;
            box-shadow: 0 0 20px rgba(77, 166, 255, 0.3);
            animation: pulse-ai 2s infinite;
        }
        @keyframes pulse-red {
            0%,
            100% {
                box-shadow: 0 0 15px rgba(200, 30, 50, 0.3);
            }
            50% {
                box-shadow: 0 0 30px rgba(255, 60, 80, 0.6);
            }
        }
        @keyframes pulse-ai {
            0%,
            100% {
                box-shadow: 0 0 15px rgba(77, 166, 255, 0.3);
            }
            50% {
                box-shadow: 0 0 30px rgba(77, 166, 255, 0.65);
            }
        }

        .turn-dot {
            width: 14px;
            height: 14px;
            border-radius: 50%;
            display: inline-block;
            flex-shrink: 0;
        }
        .turn-dot.red {
            background: #c41e3a;
            box-shadow: 0 0 8px #ff4050;
        }
        .turn-dot.black {
            background: #4da6ff;
            box-shadow: 0 0 8px #7ec8f8;
        }

        .status-message {
            font-size: 0.95em;
            color: #ffcc80;
            text-align: center;
            min-height: 1.4em;
            letter-spacing: 0.04em;
            font-weight: bold;
            transition: all 0.3s;
        }
        .status-message.check-warning {
            color: #ff5555;
            animation: flash-warning 0.6s infinite alternate;
        }
        .status-message.game-over {
            color: #ffd700;
            font-size: 1.1em;
        }
        .status-message.ai-thinking {
            color: #7ec8f8;
            animation: ai-thinking-pulse 0.8s infinite alternate;
        }
        @keyframes flash-warning {
            from {
                opacity: 0.7;
            }
            to {
                opacity: 1;
            }
        }
        @keyframes ai-thinking-pulse {
            from {
                opacity: 0.6;
            }
            to {
                opacity: 1;
            }
        }

        .canvas-wrapper {
            position: relative;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 3px #5d3a1a, 0 0 0 6px #3a1f04;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.3s;
        }
        .canvas-wrapper.ai-thinking-wrapper {
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 3px #5d3a1a, 0 0 0 6px #3a1f04,
                0 0 40px rgba(77, 166, 255, 0.35);
        }
        .canvas-wrapper:active {
            transform: scale(0.995);
        }
        .canvas-wrapper.no-interact {
            cursor: not-allowed;
            pointer-events: none;
            opacity: 0.92;
        }

        canvas {
            display: block;
            max-width: 100%;
            height: auto;
        }

        .btn-row {
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
            justify-content: center;
            align-items: center;
        }

        .btn {
            padding: 10px 22px;
            font-size: 1em;
            font-weight: bold;
            letter-spacing: 0.05em;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            transition: all 0.25s;
            font-family: inherit;
            color: #fff;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        }
        .btn-restart {
            background: #c41e3a;
        }
        .btn-restart:hover {
            background: #e02845;
            box-shadow: 0 6px 20px rgba(200, 30, 50, 0.5);
            transform: translateY(-2px);
        }
        .btn-undo {
            background: #555;
        }
        .btn-undo:hover {
            background: #6a6a6a;
            box-shadow: 0 6px 20px rgba(100, 100, 100, 0.5);
            transform: translateY(-2px);
        }
        .btn:active {
            transform: translateY(1px);
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
        }
        .btn:disabled {
            opacity: 0.4;
            cursor: not-allowed;
            pointer-events: none;
        }

        .ai-difficulty-label {
            font-size: 0.8em;
            color: #aaa;
            letter-spacing: 0.04em;
            text-align: center;
        }

        @media (max-width: 640px) {
            .game-container {
                padding: 10px 8px 14px;
                gap: 8px;
                border-radius: 14px;
            }
            .game-title {
                font-size: 1.2em;
            }
            .turn-indicator {
                font-size: 0.85em;
                padding: 6px 12px;
            }
            .btn {
                padding: 8px 16px;
                font-size: 0.9em;
            }
            .status-message {
                font-size: 0.8em;
            }
            .ai-badge {
                font-size: 0.65em;
                padding: 3px 8px;
            }
        }
    </style>
</head>
<body>

    <div class="game-container">
        <div class="game-header">
            <span class="game-title">🏯 中国象棋 <span class="ai-badge"><span class="ai-dot"></span>AI对战</span></span>
            <span class="turn-indicator red-turn" id="turnIndicator">
                <span class="turn-dot red"></span> 红方走棋(你)
            </span>
        </div>
        <div class="status-message" id="statusMessage"></div>
        <div class="canvas-wrapper" id="canvasWrapper">
            <canvas id="chessCanvas"></canvas>
        </div>
        <div class="btn-row">
            <button class="btn btn-undo" id="btnUndo" title="悔棋(撤销你上一步和AI的回应)">⟲ 悔棋</button>
            <button class="btn btn-restart" id="btnRestart">🔄 重新开始</button>
        </div>
        <div class="ai-difficulty-label">🤖 黑方AI · 搜索深度 3 层</div>
    </div>

    <script>
        (function() {
            // ==================== DOM 元素 ====================
            const canvas = document.getElementById('chessCanvas');
            const ctx = canvas.getContext('2d');
            const turnIndicator = document.getElementById('turnIndicator');
            const statusMessage = document.getElementById('statusMessage');
            const btnRestart = document.getElementById('btnRestart');
            const btnUndo = document.getElementById('btnUndo');
            const canvasWrapper = document.getElementById('canvasWrapper');

            // ==================== 常量 ====================
            const COL_COUNT = 9;
            const ROW_COUNT = 10;
            const CELL_SIZE = 58;
            const MARGIN = 52;
            const PIECE_RADIUS = 25;
            const CANVAS_WIDTH = MARGIN * 2 + CELL_SIZE * (COL_COUNT - 1);
            const CANVAS_HEIGHT = MARGIN * 2 + CELL_SIZE * (ROW_COUNT - 1);
            const AI_DELAY = 350; // AI走棋延迟(毫秒),让动画更自然

            // 设置Canvas实际尺寸
            canvas.width = CANVAS_WIDTH;
            canvas.height = CANVAS_HEIGHT;
            canvas.style.width = CANVAS_WIDTH + 'px';
            canvas.style.height = CANVAS_HEIGHT + 'px';

            function resizeCanvas() {
                const maxWidth = Math.min(window.innerWidth - 30, 600);
                if (maxWidth < CANVAS_WIDTH) {
                    const scale = maxWidth / CANVAS_WIDTH;
                    canvas.style.width = (CANVAS_WIDTH * scale) + 'px';
                    canvas.style.height = (CANVAS_HEIGHT * scale) + 'px';
                } else {
                    canvas.style.width = CANVAS_WIDTH + 'px';
                    canvas.style.height = CANVAS_HEIGHT + 'px';
                }
            }
            window.addEventListener('resize', resizeCanvas);
            resizeCanvas();

            // ==================== 游戏状态 ====================
            let board = [];
            let currentTurn = 'red';
            let selectedRow = null;
            let selectedCol = null;
            let validMoves = [];
            let gameOver = false;
            let winner = null;
            let inCheck = false;
            let moveHistory = [];
            let lastMoveInfo = null;
            let aiTimeoutId = null;
            let isAiThinking = false;

            // ==================== 棋子类型 ====================
            const PIECE_NAMES = {
                red: { king: '帅', advisor: '仕', elephant: '相', knight: '馬', rook: '車', cannon: '炮', pawn: '兵' },
                black: { king: '将', advisor: '士', elephant: '象', knight: '馬', rook: '車', cannon: '砲', pawn: '卒' },
            };

            // ==================== 棋子基础价值(从黑方AI视角) ====================
            const PIECE_BASE_VALUES = {
                king: 100000,
                rook: 900,
                cannon: 450,
                knight: 400,
                elephant: 200,
                advisor: 200,
                pawn: 100,
            };

            // ==================== 位置价值表(从黑方视角,10行×9列) ====================
            // 黑方在棋盘上方(row 0-4),红方在下方(row 5-9)
            // 正值对黑方有利
            const POSITION_VALUES = {
                // 黑方兵/卒位置价值(黑方视角:兵过河后价值大增)
                blackPawnPos: [
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [5, 10, 15, 20, 25, 20, 15, 10, 5],
                    [10, 20, 30, 45, 55, 45, 30, 20, 10],
                    [20, 35, 50, 70, 85, 70, 50, 35, 20],
                    [30, 45, 60, 80, 95, 80, 60, 45, 30],
                    [35, 50, 65, 85, 100, 85, 65, 50, 35],
                    [30, 40, 50, 60, 65, 60, 50, 40, 30],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                ],
                // 红方兵/卒位置价值(从黑方视角,红兵价值需取负)
                redPawnPos: [
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [30, 40, 50, 60, 65, 60, 50, 40, 30],
                    [35, 50, 65, 85, 100, 85, 65, 50, 35],
                    [30, 45, 60, 80, 95, 80, 60, 45, 30],
                    [20, 35, 50, 70, 85, 70, 50, 35, 20],
                    [10, 20, 30, 45, 55, 45, 30, 20, 10],
                    [5, 10, 15, 20, 25, 20, 15, 10, 5],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                ],
                // 马位置价值
                knightPos: [
                    [0, 0, 5, 10, 10, 10, 5, 0, 0],
                    [0, 10, 20, 30, 30, 30, 20, 10, 0],
                    [5, 20, 35, 45, 50, 45, 35, 20, 5],
                    [10, 25, 40, 55, 60, 55, 40, 25, 10],
                    [10, 25, 40, 55, 60, 55, 40, 25, 10],
                    [10, 25, 40, 55, 60, 55, 40, 25, 10],
                    [5, 20, 35, 45, 50, 45, 35, 20, 5],
                    [0, 10, 20, 30, 30, 30, 20, 10, 0],
                    [0, 0, 5, 10, 10, 10, 5, 0, 0],
                    [0, -5, -10, 0, 0, 0, -10, -5, 0],
                ],
                // 车位置价值
                rookPos: [
                    [10, 15, 20, 25, 30, 25, 20, 15, 10],
                    [15, 25, 30, 35, 40, 35, 30, 25, 15],
                    [10, 20, 25, 30, 35, 30, 25, 20, 10],
                    [5, 15, 20, 25, 30, 25, 20, 15, 5],
                    [0, 10, 15, 20, 25, 20, 15, 10, 0],
                    [0, 10, 15, 20, 25, 20, 15, 10, 0],
                    [5, 15, 20, 25, 30, 25, 20, 15, 5],
                    [10, 20, 25, 30, 35, 30, 25, 20, 10],
                    [15, 25, 30, 35, 40, 35, 30, 25, 15],
                    [10, 15, 20, 25, 30, 25, 20, 15, 10],
                ],
                // 炮位置价值
                cannonPos: [
                    [5, 10, 15, 20, 25, 20, 15, 10, 5],
                    [10, 20, 25, 30, 35, 30, 25, 20, 10],
                    [5, 15, 20, 25, 30, 25, 20, 15, 5],
                    [0, 10, 15, 20, 25, 20, 15, 10, 0],
                    [0, 5, 10, 15, 20, 15, 10, 5, 0],
                    [0, 5, 10, 15, 20, 15, 10, 5, 0],
                    [0, 10, 15, 20, 25, 20, 15, 10, 0],
                    [5, 15, 20, 25, 30, 25, 20, 15, 5],
                    [10, 20, 25, 30, 35, 30, 25, 20, 10],
                    [5, 10, 15, 20, 25, 20, 15, 10, 5],
                ],
            };

            // ==================== 初始化棋盘 ====================
            function createInitialBoard() {
                const b = Array.from({ length: ROW_COUNT }, () => Array(COL_COUNT).fill(null));

                const blackSetup = [
                    { row: 0, col: 0, type: 'rook' }, { row: 0, col: 1, type: 'knight' }, { row: 0, col: 2,
                    type: 'elephant' },
                    { row: 0, col: 3, type: 'advisor' }, { row: 0, col: 4, type: 'king' }, { row: 0, col: 5,
                        type: 'advisor' },
                    { row: 0, col: 6, type: 'elephant' }, { row: 0, col: 7, type: 'knight' }, { row: 0, col: 8,
                        type: 'rook' },
                    { row: 2, col: 1, type: 'cannon' }, { row: 2, col: 7, type: 'cannon' },
                    { row: 3, col: 0, type: 'pawn' }, { row: 3, col: 2, type: 'pawn' }, { row: 3, col: 4,
                    type: 'pawn' },
                    { row: 3, col: 6, type: 'pawn' }, { row: 3, col: 8, type: 'pawn' },
                ];

                const redSetup = [
                    { row: 9, col: 0, type: 'rook' }, { row: 9, col: 1, type: 'knight' }, { row: 9, col: 2,
                    type: 'elephant' },
                    { row: 9, col: 3, type: 'advisor' }, { row: 9, col: 4, type: 'king' }, { row: 9, col: 5,
                        type: 'advisor' },
                    { row: 9, col: 6, type: 'elephant' }, { row: 9, col: 7, type: 'knight' }, { row: 9, col: 8,
                        type: 'rook' },
                    { row: 7, col: 1, type: 'cannon' }, { row: 7, col: 7, type: 'cannon' },
                    { row: 6, col: 0, type: 'pawn' }, { row: 6, col: 2, type: 'pawn' }, { row: 6, col: 4,
                    type: 'pawn' },
                    { row: 6, col: 6, type: 'pawn' }, { row: 6, col: 8, type: 'pawn' },
                ];

                blackSetup.forEach(({ row, col, type }) => { b[row][col] = { type, side: 'black' }; });
                redSetup.forEach(({ row, col, type }) => { b[row][col] = { type, side: 'red' }; });

                return b;
            }

            function resetGame() {
                clearAiTimeout();
                board = createInitialBoard();
                currentTurn = 'red';
                selectedRow = null;
                selectedCol = null;
                validMoves = [];
                gameOver = false;
                winner = null;
                inCheck = false;
                moveHistory = [];
                lastMoveInfo = null;
                isAiThinking = false;
                updateUI();
                drawAll();
            }

            function clearAiTimeout() {
                if (aiTimeoutId !== null) {
                    clearTimeout(aiTimeoutId);
                    aiTimeoutId = null;
                }
                isAiThinking = false;
            }

            // ==================== 坐标转换 ====================
            function rowColToXY(row, col) {
                return { x: MARGIN + col * CELL_SIZE, y: MARGIN + row * CELL_SIZE };
            }

            function canvasToRowCol(canvasX, canvasY) {
                const rect = canvas.getBoundingClientRect();
                const scaleX = CANVAS_WIDTH / rect.width;
                const scaleY = CANVAS_HEIGHT / rect.height;
                const x = canvasX * scaleX;
                const y = canvasY * scaleY;
                const col = Math.round((x - MARGIN) / CELL_SIZE);
                const row = Math.round((y - MARGIN) / CELL_SIZE);
                const { x: nearestX, y: nearestY } = rowColToXY(row, col);
                const dist = Math.sqrt((x - nearestX) ** 2 + (y - nearestY) ** 2);
                if (dist <= PIECE_RADIUS + 6 && row >= 0 && row < ROW_COUNT && col >= 0 && col < COL_COUNT) {
                    return { row, col };
                }
                return null;
            }

            function getPieceAt(row, col) {
                if (row < 0 || row >= ROW_COUNT || col < 0 || col >= COL_COUNT) return null;
                return board[row][col];
            }

            // ==================== 查找帅/将 ====================
            function findKingInBoard(side, boardState) {
                for (let row = 0; row < ROW_COUNT; row++) {
                    for (let col = 0; col < COL_COUNT; col++) {
                        const piece = boardState[row][col];
                        if (piece && piece.type === 'king' && piece.side === side) {
                            return { row, col };
                        }
                    }
                }
                return null;
            }

            function findKing(side) {
                return findKingInBoard(side, board);
            }

            // ==================== 九宫格判断 ====================
            function isInPalace(row, col, side) {
                if (col < 3 || col > 5) return false;
                if (side === 'black') return row >= 0 && row <= 2;
                if (side === 'red') return row >= 7 && row <= 9;
                return false;
            }

            function isInOwnHalf(row, side) {
                if (side === 'black') return row >= 0 && row <= 4;
                if (side === 'red') return row >= 5 && row <= 9;
                return false;
            }

            // ==================== 帅/将安全检测 ====================
            function isKingSafe(side, boardState) {
                const kingPos = findKingInBoard(side, boardState);
                if (!kingPos) return false;
                const { row: kr, col: kc } = kingPos;
                const opponentSide = side === 'red' ? 'black' : 'red';

                for (let r = 0; r < ROW_COUNT; r++) {
                    for (let c = 0; c < COL_COUNT; c++) {
                        const piece = boardState[r][c];
                        if (piece && piece.side === opponentSide) {
                            const attacks = getRawAttacks(r, c, piece.type, piece.side, boardState);
                            if (attacks.some(a => a.row === kr && a.col === kc)) {
                                return false;
                            }
                        }
                    }
                }

                const oppKingPos = findKingInBoard(opponentSide, boardState);
                if (oppKingPos && oppKingPos.col === kc) {
                    const minRow = Math.min(kr, oppKingPos.row);
                    const maxRow = Math.max(kr, oppKingPos.row);
                    let blocked = false;
                    for (let r = minRow + 1; r < maxRow; r++) {
                        if (boardState[r][kc] !== null) { blocked = true; break; }
                    }
                    if (!blocked) return false;
                }
                return true;
            }

            // ==================== 原始攻击范围 ====================
            function getRawAttacks(row, col, type, side, boardState) {
                const moves = [];
                const opponentSide = side === 'red' ? 'black' : 'red';

                function addLineMoves(dr, dc, maxSteps = 20) {
                    for (let i = 1; i <= maxSteps; i++) {
                        const r = row + dr * i;
                        const c = col + dc * i;
                        if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
                        const piece = boardState[r][c];
                        if (piece) {
                            if (piece.side === opponentSide) moves.push({ row: r, col: c });
                            break;
                        }
                        moves.push({ row: r, col: c });
                    }
                }

                switch (type) {
                    case 'king':
                        for (const [dr, dc] of [
                                [-1, 0],
                                [1, 0],
                                [0, -1],
                                [0, 1]
                            ]) {
                            const r = row + dr;
                            const c = col + dc;
                            if (isInPalace(r, c, side)) {
                                const target = boardState[r][c];
                                if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
                            }
                        }
                        break;
                    case 'advisor':
                        for (const [dr, dc] of [
                                [-1, -1],
                                [-1, 1],
                                [1, -1],
                                [1, 1]
                            ]) {
                            const r = row + dr;
                            const c = col + dc;
                            if (isInPalace(r, c, side)) {
                                const target = boardState[r][c];
                                if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
                            }
                        }
                        break;
                    case 'elephant':
                        for (const [dr, dc, eyeDr, eyeDc] of [
                                [-2, -2, -1, -1],
                                [-2, 2, -1, 1],
                                [2, -2, 1, -1],
                                [2, 2, 1, 1]
                            ]) {
                            const r = row + dr;
                            const c = col + dc;
                            const eyeR = row + eyeDr;
                            const eyeC = col + eyeDc;
                            if (r >= 0 && r < ROW_COUNT && c >= 0 && c < COL_COUNT &&
                                isInOwnHalf(r, side) && !boardState[eyeR][eyeC]) {
                                const target = boardState[r][c];
                                if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
                            }
                        }
                        break;
                    case 'knight':
                        const knightMoves = [
                            { dr: -2, dc: -1, legR: -1, legC: 0 }, { dr: -2, dc: 1, legR: -1, legC: 0 },
                            { dr: 2, dc: -1, legR: 1, legC: 0 }, { dr: 2, dc: 1, legR: 1, legC: 0 },
                            { dr: -1, dc: -2, legR: 0, legC: -1 }, { dr: -1, dc: 2, legR: 0, legC: 1 },
                            { dr: 1, dc: -2, legR: 0, legC: -1 }, { dr: 1, dc: 2, legR: 0, legC: 1 },
                        ];
                        for (const { dr, dc, legR, legC } of knightMoves) {
                            const r = row + dr;
                            const c = col + dc;
                            const lr = row + legR;
                            const lc = col + legC;
                            if (r >= 0 && r < ROW_COUNT && c >= 0 && c < COL_COUNT && !boardState[lr][lc]) {
                                const target = boardState[r][c];
                                if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
                            }
                        }
                        break;
                    case 'rook':
                        addLineMoves(-1, 0);
                        addLineMoves(1, 0);
                        addLineMoves(0, -1);
                        addLineMoves(0, 1);
                        break;
                    case 'cannon':
                        for (const [dr, dc] of [
                                [-1, 0],
                                [1, 0],
                                [0, -1],
                                [0, 1]
                            ]) {
                            for (let i = 1; i < 20; i++) {
                                const r = row + dr * i;
                                const c = col + dc * i;
                                if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
                                if (boardState[r][c]) break;
                                moves.push({ row: r, col: c });
                            }
                        }
                        for (const [dr, dc] of [
                                [-1, 0],
                                [1, 0],
                                [0, -1],
                                [0, 1]
                            ]) {
                            let foundPlatform = false;
                            for (let i = 1; i < 20; i++) {
                                const r = row + dr * i;
                                const c = col + dc * i;
                                if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
                                const piece = boardState[r][c];
                                if (!foundPlatform) {
                                    if (piece) foundPlatform = true;
                                } else {
                                    if (piece) {
                                        if (piece.side === opponentSide) moves.push({ row: r, col: c });
                                        break;
                                    }
                                }
                            }
                        }
                        break;
                    case 'pawn':
                        if (side === 'red') {
                            const forwardR = row - 1;
                            if (forwardR >= 0) {
                                const target = boardState[forwardR][col];
                                if (!target || target.side === opponentSide) moves.push({ row: forwardR, col: col });
                            }
                            if (!isInOwnHalf(row, side)) {
                                for (const dc of [-1, 1]) {
                                    const c = col + dc;
                                    if (c >= 0 && c < COL_COUNT) {
                                        const target = boardState[row][c];
                                        if (!target || target.side === opponentSide) moves.push({ row: row, col: c });
                                    }
                                }
                            }
                        } else {
                            const forwardR = row + 1;
                            if (forwardR < ROW_COUNT) {
                                const target = boardState[forwardR][col];
                                if (!target || target.side === opponentSide) moves.push({ row: forwardR, col: col });
                            }
                            if (!isInOwnHalf(row, side)) {
                                for (const dc of [-1, 1]) {
                                    const c = col + dc;
                                    if (c >= 0 && c < COL_COUNT) {
                                        const target = boardState[row][c];
                                        if (!target || target.side === opponentSide) moves.push({ row: row, col: c });
                                    }
                                }
                            }
                        }
                        break;
                }
                return moves;
            }

            // ==================== 合法移动 ====================
            function getLegalMovesForBoard(row, col, boardState) {
                const piece = boardState[row][col];
                if (!piece) return [];
                const rawMoves = getRawAttacks(row, col, piece.type, piece.side, boardState);
                const legalMoves = [];
                for (const move of rawMoves) {
                    const newBoard = boardState.map(r => [...r]);
                    newBoard[move.row][move.col] = newBoard[row][col];
                    newBoard[row][col] = null;
                    if (isKingSafe(piece.side, newBoard)) {
                        legalMoves.push(move);
                    }
                }
                return legalMoves;
            }

            function getLegalMoves(row, col) {
                return getLegalMovesForBoard(row, col, board);
            }

            function hasAnyLegalMoveForBoard(side, boardState) {
                for (let r = 0; r < ROW_COUNT; r++) {
                    for (let c = 0; c < COL_COUNT; c++) {
                        const piece = boardState[r][c];
                        if (piece && piece.side === side) {
                            if (getLegalMovesForBoard(r, c, boardState).length > 0) return true;
                        }
                    }
                }
                return false;
            }

            function hasAnyLegalMove(side) {
                return hasAnyLegalMoveForBoard(side, board);
            }

            function checkIfInCheckForBoard(side, boardState) {
                return !isKingSafe(side, boardState);
            }

            function checkIfInCheck(side) {
                return checkIfInCheckForBoard(side, board);
            }

            // ==================== 执行移动 ====================
            function executeMove(fromRow, fromCol, toRow, toCol, recordHistory = true) {
                const piece = board[fromRow][fromCol];
                const captured = board[toRow][toCol];

                if (recordHistory) {
                    moveHistory.push({
                        fromRow,
                        fromCol,
                        toRow,
                        toCol,
                        piece,
                        captured,
                        prevInCheck: inCheck,
                    });
                }

                board[toRow][toCol] = piece;
                board[fromRow][fromCol] = null;
                lastMoveInfo = { fromRow, fromCol, toRow, toCol };

                const prevTurn = currentTurn;
                currentTurn = currentTurn === 'red' ? 'black' : 'red';
                inCheck = checkIfInCheck(currentTurn);
                const opponentHasMoves = hasAnyLegalMove(currentTurn);

                if (!opponentHasMoves) {
                    gameOver = true;
                    winner = prevTurn;
                    inCheck = false;
                }

                selectedRow = null;
                selectedCol = null;
                validMoves = [];
            }

            // ==================== 悔棋 ====================
            function undoMove() {
                clearAiTimeout();

                if (gameOver) {
                    gameOver = false;
                    winner = null;
                    if (moveHistory.length > 0) {
                        const last = moveHistory.pop();
                        board[last.fromRow][last.fromCol] = last.piece;
                        board[last.toRow][last.toCol] = last.captured;
                        currentTurn = last.piece.side;
                        inCheck = last.prevInCheck;
                        lastMoveInfo = moveHistory.length > 0 ? {
                            fromRow: moveHistory[moveHistory.length - 1].fromRow,
                            fromCol: moveHistory[moveHistory.length - 1].fromCol,
                            toRow: moveHistory[moveHistory.length - 1].toRow,
                            toCol: moveHistory[moveHistory.length - 1].toCol,
                        } : null;
                    }
                    isAiThinking = false;
                    selectedRow = null;
                    selectedCol = null;
                    validMoves = [];
                    updateUI();
                    drawAll();
                    return;
                }

                if (moveHistory.length === 0) return;

                // 如果AI正在思考,取消并撤销红方最后一步
                if (isAiThinking) {
                    isAiThinking = false;
                    const last = moveHistory.pop();
                    board[last.fromRow][last.fromCol] = last.piece;
                    board[last.toRow][last.toCol] = last.captured;
                    currentTurn = last.piece.side;
                    inCheck = last.prevInCheck;
                    gameOver = false;
                    winner = null;
                    lastMoveInfo = moveHistory.length > 0 ? {
                        fromRow: moveHistory[moveHistory.length - 1].fromRow,
                        fromCol: moveHistory[moveHistory.length - 1].fromCol,
                        toRow: moveHistory[moveHistory.length - 1].toRow,
                        toCol: moveHistory[moveHistory.length - 1].toCol,
                    } : null;
                    selectedRow = null;
                    selectedCol = null;
                    validMoves = [];
                    updateUI();
                    drawAll();
                    return;
                }

                // 正常情况:当前是红方回合,需要撤销AI的黑步和玩家的红步(共2步)
                if (currentTurn === 'red' && moveHistory.length >= 2) {
                    // 撤销AI的步
                    const aiMove = moveHistory.pop();
                    board[aiMove.fromRow][aiMove.fromCol] = aiMove.piece;
                    board[aiMove.toRow][aiMove.toCol] = aiMove.captured;
                    // 撤销玩家的步
                    const playerMove = moveHistory.pop();
                    board[playerMove.fromRow][playerMove.fromCol] = playerMove.piece;
                    board[playerMove.toRow][playerMove.toCol] = playerMove.captured;
                    currentTurn = 'red';
                    inCheck = playerMove.prevInCheck;
                    gameOver = false;
                    winner = null;
                    lastMoveInfo = moveHistory.length > 0 ? {
                        fromRow: moveHistory[moveHistory.length - 1].fromRow,
                        fromCol: moveHistory[moveHistory.length - 1].fromCol,
                        toRow: moveHistory[moveHistory.length - 1].toRow,
                        toCol: moveHistory[moveHistory.length - 1].toCol,
                    } : null;
                } else if (currentTurn === 'red' && moveHistory.length === 1) {
                    const last = moveHistory.pop();
                    board[last.fromRow][last.fromCol] = last.piece;
                    board[last.toRow][last.toCol] = last.captured;
                    currentTurn = 'red';
                    inCheck = last.prevInCheck;
                    gameOver = false;
                    winner = null;
                    lastMoveInfo = null;
                } else if (currentTurn === 'black' && moveHistory.length >= 1) {
                    // 罕见情况:红方刚走完,AI还没被触发
                    const last = moveHistory.pop();
                    board[last.fromRow][last.fromCol] = last.piece;
                    board[last.toRow][last.toCol] = last.captured;
                    currentTurn = 'red';
                    inCheck = last.prevInCheck;
                    gameOver = false;
                    winner = null;
                    lastMoveInfo = moveHistory.length > 0 ? {
                        fromRow: moveHistory[moveHistory.length - 1].fromRow,
                        fromCol: moveHistory[moveHistory.length - 1].fromCol,
                        toRow: moveHistory[moveHistory.length - 1].toRow,
                        toCol: moveHistory[moveHistory.length - 1].toCol,
                    } : null;
                }

                isAiThinking = false;
                selectedRow = null;
                selectedCol = null;
                validMoves = [];
                updateUI();
                drawAll();
            }

            // ==================== AI 评估函数 ====================
            function evaluateBoard(boardState) {
                // 从黑方视角评估:正值 = 黑方优势
                let score = 0;

                for (let row = 0; row < ROW_COUNT; row++) {
                    for (let col = 0; col < COL_COUNT; col++) {
                        const piece = boardState[row][col];
                        if (!piece) continue;

                        const baseValue = PIECE_BASE_VALUES[piece.type] || 0;
                        let posValue = 0;

                        // 位置价值
                        if (piece.type === 'pawn') {
                            if (piece.side === 'black') {
                                posValue = (POSITION_VALUES.blackPawnPos[row] &&
                                    POSITION_VALUES.blackPawnPos[row][col]) || 0;
                            } else {
                                posValue = (POSITION_VALUES.redPawnPos[row] &&
                                    POSITION_VALUES.redPawnPos[row][col]) || 0;
                            }
                        } else if (piece.type === 'knight') {
                            posValue = (POSITION_VALUES.knightPos[row] && POSITION_VALUES.knightPos[row][col]) ||
                                0;
                        } else if (piece.type === 'rook') {
                            posValue = (POSITION_VALUES.rookPos[row] && POSITION_VALUES.rookPos[row][col]) || 0;
                        } else if (piece.type === 'cannon') {
                            posValue = (POSITION_VALUES.cannonPos[row] && POSITION_VALUES.cannonPos[row][col]) ||
                                0;
                        }

                        const totalPieceValue = baseValue + posValue;

                        if (piece.side === 'black') {
                            score += totalPieceValue;
                        } else {
                            score -= totalPieceValue;
                        }
                    }
                }

                // 额外:检查黑方是否被将军
                if (checkIfInCheckForBoard('black', boardState)) {
                    score -= 350; // 黑方被将军,惩罚
                }
                // 检查红方是否被将军
                if (checkIfInCheckForBoard('red', boardState)) {
                    score += 350; // 红方被将军,奖励
                }

                return score;
            }

            // ==================== Alpha-Beta 搜索 ====================
            function alphaBeta(boardState, depth, alpha, beta, isMaximizing, side) {
                // side: 当前走棋方
                if (depth === 0) {
                    return evaluateBoard(boardState);
                }

                const opponentSide = side === 'black' ? 'red' : 'black';

                // 检查当前走棋方是否有合法走法
                if (!hasAnyLegalMoveForBoard(side, boardState)) {
                    // 当前方被将死
                    if (side === 'black') {
                        return -100000 - depth; // 黑方输了,非常不利
                    } else {
                        return 100000 + depth; // 红方输了,非常有利
                    }
                }

                // 收集所有走法
                const allMoves = [];
                for (let r = 0; r < ROW_COUNT; r++) {
                    for (let c = 0; c < COL_COUNT; c++) {
                        const piece = boardState[r][c];
                        if (piece && piece.side === side) {
                            const moves = getLegalMovesForBoard(r, c, boardState);
                            for (const move of moves) {
                                allMoves.push({
                                    fromRow: r,
                                    fromCol: c,
                                    toRow: move.row,
                                    toCol: move.col,
                                    captured: boardState[move.row][move.col],
                                });
                            }
                        }
                    }
                }

                // 走法排序:吃子走法优先(提高剪枝效率)
                allMoves.sort((a, b) => {
                    const valA = a.captured ? (PIECE_BASE_VALUES[a.captured.type] || 0) : 0;
                    const valB = b.captured ? (PIECE_BASE_VALUES[b.captured.type] || 0) : 0;
                    return valB - valA;
                });

                if (isMaximizing) {
                    // 黑方(AI)走棋,最大化
                    let maxEval = -Infinity;
                    for (const move of allMoves) {
                        // 执行走法(原地修改)
                        const piece = boardState[move.fromRow][move.fromCol];
                        const captured = boardState[move.toRow][move.toCol];
                        boardState[move.toRow][move.toCol] = piece;
                        boardState[move.fromRow][move.fromCol] = null;

                        const evalScore = alphaBeta(boardState, depth - 1, alpha, beta, false, opponentSide);

                        // 撤销走法
                        boardState[move.fromRow][move.fromCol] = piece;
                        boardState[move.toRow][move.toCol] = captured;

                        maxEval = Math.max(maxEval, evalScore);
                        alpha = Math.max(alpha, evalScore);
                        if (beta <= alpha) break; // 剪枝
                    }
                    return maxEval;
                } else {
                    // 红方走棋,最小化
                    let minEval = Infinity;
                    for (const move of allMoves) {
                        const piece = boardState[move.fromRow][move.fromCol];
                        const captured = boardState[move.toRow][move.toCol];
                        boardState[move.toRow][move.toCol] = piece;
                        boardState[move.fromRow][move.fromCol] = null;

                        const evalScore = alphaBeta(boardState, depth - 1, alpha, beta, true, opponentSide);

                        boardState[move.fromRow][move.fromCol] = piece;
                        boardState[move.toRow][move.toCol] = captured;

                        minEval = Math.min(minEval, evalScore);
                        beta = Math.min(beta, evalScore);
                        if (beta <= alpha) break;
                    }
                    return minEval;
                }
            }

            // ==================== AI 选择最佳走法 ====================
            function findBestAIMove() {
                const side = 'black';
                const searchDepth = 3;

                const allMoves = [];
                for (let r = 0; r < ROW_COUNT; r++) {
                    for (let c = 0; c < COL_COUNT; c++) {
                        const piece = board[r][c];
                        if (piece && piece.side === side) {
                            const moves = getLegalMoves(r, c);
                            for (const move of moves) {
                                allMoves.push({
                                    fromRow: r,
                                    fromCol: c,
                                    toRow: move.row,
                                    toCol: move.col,
                                    captured: board[move.row][move.col],
                                });
                            }
                        }
                    }
                }

                if (allMoves.length === 0) return null;

                // 走法排序
                allMoves.sort((a, b) => {
                    const valA = a.captured ? (PIECE_BASE_VALUES[a.captured.type] || 0) : 0;
                    const valB = b.captured ? (PIECE_BASE_VALUES[b.captured.type] || 0) : 0;
                    return valB - valA;
                });

                let bestMove = allMoves[0];
                let bestScore = -Infinity;
                const alpha = -Infinity;
                const beta = Infinity;

                // 创建工作棋盘(深拷贝一次)
                const workBoard = board.map(r => [...r]);

                for (const move of allMoves) {
                    const piece = workBoard[move.fromRow][move.fromCol];
                    const captured = workBoard[move.toRow][move.toCol];
                    workBoard[move.toRow][move.toCol] = piece;
                    workBoard[move.fromRow][move.fromCol] = null;

                    const score = alphaBeta(workBoard, searchDepth - 1, alpha, beta, false, 'red');

                    // 撤销
                    workBoard[move.fromRow][move.fromCol] = piece;
                    workBoard[move.toRow][move.toCol] = captured;

                    if (score > bestScore) {
                        bestScore = score;
                        bestMove = move;
                    }
                }

                // 如果有多个得分相同的走法,随机选择(增加变化)
                const topMoves = allMoves.filter(move => {
                    const piece = workBoard[move.fromRow][move.fromCol];
                    const captured = workBoard[move.toRow][move.toCol];
                    workBoard[move.toRow][move.toCol] = piece;
                    workBoard[move.fromRow][move.fromCol] = null;
                    const s = alphaBeta(workBoard, searchDepth - 1, -Infinity, Infinity, false, 'red');
                    workBoard[move.fromRow][move.fromCol] = piece;
                    workBoard[move.toRow][move.toCol] = captured;
                    return Math.abs(s - bestScore) < 5;
                });

                if (topMoves.length > 1) {
                    bestMove = topMoves[Math.floor(Math.random() * topMoves.length)];
                }

                return bestMove;
            }

            // ==================== AI 走棋触发 ====================
            function triggerAI() {
                if (gameOver) return;
                if (currentTurn !== 'black') return;
                if (isAiThinking) return;

                isAiThinking = true;
                updateUI();
                drawAll();

                aiTimeoutId = setTimeout(() => {
                    aiTimeoutId = null;
                    if (gameOver || currentTurn !== 'black') {
                        isAiThinking = false;
                        updateUI();
                        drawAll();
                        return;
                    }

                    const bestMove = findBestAIMove();
                    if (!bestMove) {
                        // AI没有合法走法,应该已经被将死
                        gameOver = true;
                        winner = 'red';
                        inCheck = false;
                        isAiThinking = false;
                        updateUI();
                        drawAll();
                        return;
                    }

                    executeMove(bestMove.fromRow, bestMove.fromCol, bestMove.toRow, bestMove.toCol, true);
                    isAiThinking = false;
                    updateUI();
                    drawAll();

                    // AI走完后,如果游戏还没结束,轮到红方
                    // 不需要额外操作
                }, AI_DELAY);
            }

            // ==================== 绘制 ====================
            function drawAll() {
                ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
                drawBoardBackground();
                drawGridLines();
                drawRiver();
                drawPalaceDiagonals();
                drawLastMoveHighlight();
                drawValidMoves();
                drawAllPieces();
                drawSelectionHighlight();
                drawCheckHighlight();
            }

            function drawBoardBackground() {
                const bgGrad = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
                bgGrad.addColorStop(0, '#f5deb3');
                bgGrad.addColorStop(0.5, '#faf0dc');
                bgGrad.addColorStop(1, '#e8d5a0');
                ctx.fillStyle = bgGrad;
                ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

                ctx.strokeStyle = '#5d3a1a';
                ctx.lineWidth = 4;
                const bx = MARGIN - 18;
                const by = MARGIN - 18;
                const bw = CELL_SIZE * (COL_COUNT - 1) + 36;
                const bh = CELL_SIZE * (ROW_COUNT - 1) + 36;
                ctx.strokeRect(bx, by, bw, bh);

                ctx.strokeStyle = '#3a1f04';
                ctx.lineWidth = 1.5;
                ctx.strokeRect(bx - 3, by - 3, bw + 6, bh + 6);
            }

            function drawGridLines() {
                const startX = MARGIN;
                const startY = MARGIN;
                const endX = MARGIN + CELL_SIZE * (COL_COUNT - 1);
                const endY = MARGIN + CELL_SIZE * (ROW_COUNT - 1);
                const midY = MARGIN + CELL_SIZE * 4;
                const midY2 = MARGIN + CELL_SIZE * 5;

                ctx.strokeStyle = '#4a2a0a';
                ctx.lineWidth = 1.2;

                for (let row = 0; row <= 4; row++) {
                    const y = MARGIN + row * CELL_SIZE;
                    ctx.beginPath();
                    ctx.moveTo(startX, y);
                    ctx.lineTo(endX, y);
                    ctx.stroke();
                }
                for (let row = 5; row <= 9; row++) {
                    const y = MARGIN + row * CELL_SIZE;
                    ctx.beginPath();
                    ctx.moveTo(startX, y);
                    ctx.lineTo(endX, y);
                    ctx.stroke();
                }
                for (let col = 0; col < COL_COUNT; col++) {
                    const x = MARGIN + col * CELL_SIZE;
                    ctx.beginPath();
                    ctx.moveTo(x, startY);
                    ctx.lineTo(x, midY);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.moveTo(x, midY2);
                    ctx.lineTo(x, endY);
                    ctx.stroke();
                }
                ctx.beginPath();
                ctx.moveTo(startX, startY);
                ctx.lineTo(startX, endY);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(endX, startY);
                ctx.lineTo(endX, endY);
                ctx.stroke();
            }

            function drawRiver() {
                const midY1 = MARGIN + CELL_SIZE * 4;
                const midY2 = MARGIN + CELL_SIZE * 5;
                const startX = MARGIN;
                const endX = MARGIN + CELL_SIZE * (COL_COUNT - 1);
                const centerY = (midY1 + midY2) / 2;

                ctx.fillStyle = '#3a1f04';
                ctx.font = 'bold 22px "STKaiti","KaiTi","楷体","STSong","宋体",serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                const leftCenterX = MARGIN + CELL_SIZE * 2;
                const rightCenterX = MARGIN + CELL_SIZE * 6;
                ctx.fillText('楚  河', leftCenterX, centerY);
                ctx.fillText('汉  界', rightCenterX, centerY);

                ctx.strokeStyle = '#4a2a0a';
                ctx.lineWidth = 0.5;
                ctx.setLineDash([4, 8]);
                ctx.beginPath();
                ctx.moveTo(startX + 8, centerY);
                ctx.lineTo(endX - 8, centerY);
                ctx.stroke();
                ctx.setLineDash([]);
            }

            function drawPalaceDiagonals() {
                ctx.strokeStyle = '#4a2a0a';
                ctx.lineWidth = 0.9;
                ctx.setLineDash([3, 3]);

                const blackTopLeft = rowColToXY(0, 3);
                const blackTopRight = rowColToXY(0, 5);
                const blackBotLeft = rowColToXY(2, 3);
                const blackBotRight = rowColToXY(2, 5);
                ctx.beginPath();
                ctx.moveTo(blackTopLeft.x, blackTopLeft.y);
                ctx.lineTo(blackBotRight.x, blackBotRight.y);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(blackTopRight.x, blackTopRight.y);
                ctx.lineTo(blackBotLeft.x, blackBotLeft.y);
                ctx.stroke();

                const redTopLeft = rowColToXY(7, 3);
                const redTopRight = rowColToXY(7, 5);
                const redBotLeft = rowColToXY(9, 3);
                const redBotRight = rowColToXY(9, 5);
                ctx.beginPath();
                ctx.moveTo(redTopLeft.x, redTopLeft.y);
                ctx.lineTo(redBotRight.x, redBotRight.y);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(redTopRight.x, redTopRight.y);
                ctx.lineTo(redBotLeft.x, redBotLeft.y);
                ctx.stroke();

                ctx.setLineDash([]);
            }

            function drawLastMoveHighlight() {
                if (!lastMoveInfo) return;
                const { fromRow, fromCol, toRow, toCol } = lastMoveInfo;

                const highlightSquare = (row, col) => {
                    const { x, y } = rowColToXY(row, col);
                    const s = CELL_SIZE * 0.55;
                    ctx.fillStyle = 'rgba(255, 200, 50, 0.35)';
                    ctx.fillRect(x - s / 2, y - s / 2, s, s);
                    ctx.strokeStyle = 'rgba(200, 150, 30, 0.6)';
                    ctx.lineWidth = 2;
                    ctx.strokeRect(x - s / 2, y - s / 2, s, s);
                };

                highlightSquare(fromRow, fromCol);
                highlightSquare(toRow, toCol);
            }

            function drawValidMoves() {
                for (const move of validMoves) {
                    const { x, y } = rowColToXY(move.row, move.col);
                    const targetPiece = board[move.row][move.col];

                    if (targetPiece) {
                        ctx.beginPath();
                        ctx.arc(x, y, PIECE_RADIUS + 3, 0, Math.PI * 2);
                        ctx.setLineDash([5, 3]);
                        ctx.strokeStyle = 'rgba(220,40,40,0.75)';
                        ctx.lineWidth = 3;
                        ctx.stroke();
                        ctx.setLineDash([]);
                        ctx.fillStyle = 'rgba(255,100,100,0.2)';
                        ctx.fill();
                    } else {
                        ctx.beginPath();
                        ctx.arc(x, y, 8, 0, Math.PI * 2);
                        ctx.fillStyle = 'rgba(80,160,80,0.55)';
                        ctx.fill();
                        ctx.strokeStyle = 'rgba(50,120,50,0.5)';
                        ctx.lineWidth = 1.5;
                        ctx.stroke();
                    }
                }
            }

            function drawAllPieces() {
                for (let row = 0; row < ROW_COUNT; row++) {
                    for (let col = 0; col < COL_COUNT; col++) {
                        const piece = board[row][col];
                        if (piece) {
                            drawPiece(row, col, piece);
                        }
                    }
                }
            }

            function drawPiece(row, col, piece) {
                const { x, y } = rowColToXY(row, col);
                const isSelected = (selectedRow === row && selectedCol === col);
                const isRed = piece.side === 'red';
                const pieceColor = isRed ? '#c41e3a' : '#1a1a1a';

                // 阴影
                ctx.beginPath();
                ctx.arc(x + 2, y + 3, PIECE_RADIUS, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(0,0,0,0.3)';
                ctx.fill();

                // 棋子主体渐变
                const pieceGrad = ctx.createRadialGradient(x - 6, y - 8, PIECE_RADIUS * 0.1, x, y, PIECE_RADIUS);
                pieceGrad.addColorStop(0, '#ffffff');
                pieceGrad.addColorStop(0.55, '#f5f0e8');
                pieceGrad.addColorStop(0.85, '#e0d5c0');
                pieceGrad.addColorStop(1, '#c8b898');
                ctx.beginPath();
                ctx.arc(x, y, PIECE_RADIUS, 0, Math.PI * 2);
                ctx.fillStyle = pieceGrad;
                ctx.fill();

                // 边框
                ctx.beginPath();
                ctx.arc(x, y, PIECE_RADIUS, 0, Math.PI * 2);
                ctx.strokeStyle = pieceColor;
                ctx.lineWidth = 2.5;
                ctx.stroke();

                // 内圈
                ctx.beginPath();
                ctx.arc(x, y, PIECE_RADIUS - 4, 0, Math.PI * 2);
                ctx.strokeStyle = pieceColor;
                ctx.lineWidth = 1;
                ctx.setLineDash([3, 2]);
                ctx.stroke();
                ctx.setLineDash([]);

                // 选中高亮
                if (isSelected) {
                    ctx.beginPath();
                    ctx.arc(x, y, PIECE_RADIUS + 5, 0, Math.PI * 2);
                    ctx.strokeStyle = '#ffb800';
                    ctx.lineWidth = 3.5;
                    ctx.shadowColor = '#ffd700';
                    ctx.shadowBlur = 15;
                    ctx.stroke();
                    ctx.shadowColor = 'transparent';
                    ctx.shadowBlur = 0;

                    ctx.beginPath();
                    ctx.arc(x, y, PIECE_RADIUS + 2, 0, Math.PI * 2);
                    ctx.strokeStyle = 'rgba(255,200,50,0.5)';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                }

                // 文字
                const name = PIECE_NAMES[piece.side][piece.type];
                ctx.fillStyle = pieceColor;
                ctx.font = 'bold 24px "STKaiti","KaiTi","楷体","STSong","宋体","Noto Serif SC",serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(name, x, y + 1);

                // 将军高亮
                if (inCheck && piece.type === 'king' && piece.side === currentTurn && !gameOver) {
                    ctx.beginPath();
                    ctx.arc(x, y, PIECE_RADIUS + 3, 0, Math.PI * 2);
                    ctx.strokeStyle = 'rgba(255,40,40,0.8)';
                    ctx.lineWidth = 3;
                    ctx.setLineDash([6, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);
                }
            }

            function drawSelectionHighlight() {
                if (selectedRow === null || selectedCol === null) return;
                const { x, y } = rowColToXY(selectedRow, selectedCol);
                ctx.beginPath();
                ctx.arc(x, y, PIECE_RADIUS + 7, 0, Math.PI * 2);
                const glowGrad = ctx.createRadialGradient(x, y, PIECE_RADIUS, x, y, PIECE_RADIUS + 8);
                glowGrad.addColorStop(0, 'rgba(255,200,60,0)');
                glowGrad.addColorStop(1, 'rgba(255,200,60,0.35)');
                ctx.fillStyle = glowGrad;
                ctx.fill();
            }

            function drawCheckHighlight() {
                if (!inCheck || gameOver) return;
                const kingPos = findKing(currentTurn);
                if (!kingPos) return;
                const { x, y } = rowColToXY(kingPos.row, kingPos.col);
                const pulseGrad = ctx.createRadialGradient(x, y, PIECE_RADIUS + 2, x, y, PIECE_RADIUS + 10);
                pulseGrad.addColorStop(0, 'rgba(255,60,60,0)');
                pulseGrad.addColorStop(1, 'rgba(255,30,30,0.25)');
                ctx.beginPath();
                ctx.arc(x, y, PIECE_RADIUS + 10, 0, Math.PI * 2);
                ctx.fillStyle = pulseGrad;
                ctx.fill();
            }

            // ==================== UI更新 ====================
            function updateUI() {
                // Canvas wrapper状态
                if (isAiThinking) {
                    canvasWrapper.classList.add('ai-thinking-wrapper');
                    canvasWrapper.classList.add('no-interact');
                } else if (gameOver) {
                    canvasWrapper.classList.remove('ai-thinking-wrapper');
                    canvasWrapper.classList.add('no-interact');
                } else if (currentTurn === 'black') {
                    canvasWrapper.classList.remove('ai-thinking-wrapper');
                    canvasWrapper.classList.add('no-interact');
                } else {
                    canvasWrapper.classList.remove('ai-thinking-wrapper');
                    canvasWrapper.classList.remove('no-interact');
                }

                // 状态消息
                if (gameOver) {
                    turnIndicator.className = 'turn-indicator';
                    turnIndicator.innerHTML =
                        '<span class="turn-dot" style="background:#ffd700;box-shadow:0 0 10px #ffd700;"></span> 游戏结束';
                    if (winner === 'red') {
                        statusMessage.textContent = '🏆 恭喜!红方(你)获胜!黑方无棋可走。';
                    } else {
                        statusMessage.textContent = '🤖 黑方(AI)获胜!红方无棋可走。';
                    }
                    statusMessage.className = 'status-message game-over';
                    btnUndo.disabled = false;
                } else if (isAiThinking) {
                    turnIndicator.className = 'turn-indicator black-turn';
                    turnIndicator.innerHTML =
                        '<span class="turn-dot black"></span> 黑方思考中... <span style="font-size:0.8em;">🤖</span>';
                    statusMessage.textContent = '🤖 AI正在计算最佳走法...';
                    statusMessage.className = 'status-message ai-thinking';
                    btnUndo.disabled = false;
                } else if (inCheck && currentTurn === 'red') {
                    turnIndicator.className = 'turn-indicator red-turn';
                    turnIndicator.innerHTML =
                        '<span class="turn-dot red"></span> 红方走棋(你) <span style="color:#ff4444;font-size:0.85em;">⚠将军</span>';
                    statusMessage.textContent = '⚠ 你的帅被将军!必须应将。';
                    statusMessage.className = 'status-message check-warning';
                    btnUndo.disabled = moveHistory.length < 2;
                } else if (inCheck && currentTurn === 'black') {
                    turnIndicator.className = 'turn-indicator black-turn';
                    turnIndicator.innerHTML =
                        '<span class="turn-dot black"></span> 黑方走棋(AI) <span style="color:#ff4444;font-size:0.85em;">⚠将军</span>';
                    statusMessage.textContent = '⚠ 黑方被将军,AI正在应对...';
                    statusMessage.className = 'status-message check-warning';
                    btnUndo.disabled = true;
                } else if (currentTurn === 'red') {
                    turnIndicator.className = 'turn-indicator red-turn';
                    turnIndicator.innerHTML = '<span class="turn-dot red"></span> 红方走棋(你)';
                    statusMessage.textContent = '';
                    statusMessage.className = 'status-message';
                    btnUndo.disabled = moveHistory.length < 2;
                } else {
                    turnIndicator.className = 'turn-indicator black-turn';
                    turnIndicator.innerHTML = '<span class="turn-dot black"></span> 黑方走棋(AI)';
                    statusMessage.textContent = '';
                    statusMessage.className = 'status-message';
                    btnUndo.disabled = true;
                }
            }

            // ==================== 事件处理 ====================
            canvas.addEventListener('click', function(e) {
                if (gameOver) return;
                if (isAiThinking) return;
                if (currentTurn !== 'red') return; // 只有红方(玩家)可以手动走棋

                const rect = canvas.getBoundingClientRect();
                const canvasX = e.clientX - rect.left;
                const canvasY = e.clientY - rect.top;

                const pos = canvasToRowCol(canvasX, canvasY);
                if (!pos) {
                    selectedRow = null;
                    selectedCol = null;
                    validMoves = [];
                    drawAll();
                    return;
                }

                const { row, col } = pos;
                const clickedPiece = getPieceAt(row, col);

                if (selectedRow !== null && selectedCol !== null) {
                    const isValidTarget = validMoves.some(m => m.row === row && m.col === col);
                    if (isValidTarget) {
                        executeMove(selectedRow, selectedCol, row, col, true);
                        updateUI();
                        drawAll();
                        // 检查是否需要触发AI
                        if (!gameOver && currentTurn === 'black') {
                            triggerAI();
                        }
                        return;
                    }
                }

                if (clickedPiece && clickedPiece.side === 'red') {
                    selectedRow = row;
                    selectedCol = col;
                    validMoves = getLegalMoves(row, col);
                    drawAll();
                    return;
                }

                selectedRow = null;
                selectedCol = null;
                validMoves = [];
                drawAll();
            });

            canvas.addEventListener('touchstart', function(e) {
                e.preventDefault();
                if (gameOver) return;
                if (isAiThinking) return;
                if (currentTurn !== 'red') return;

                const touch = e.touches[0];
                const rect = canvas.getBoundingClientRect();
                const canvasX = touch.clientX - rect.left;
                const canvasY = touch.clientY - rect.top;

                const pos = canvasToRowCol(canvasX, canvasY);
                if (!pos) {
                    selectedRow = null;
                    selectedCol = null;
                    validMoves = [];
                    drawAll();
                    return;
                }

                const { row, col } = pos;
                const clickedPiece = getPieceAt(row, col);

                if (selectedRow !== null && selectedCol !== null) {
                    const isValidTarget = validMoves.some(m => m.row === row && m.col === col);
                    if (isValidTarget) {
                        executeMove(selectedRow, selectedCol, row, col, true);
                        updateUI();
                        drawAll();
                        if (!gameOver && currentTurn === 'black') {
                            triggerAI();
                        }
                        return;
                    }
                }

                if (clickedPiece && clickedPiece.side === 'red') {
                    selectedRow = row;
                    selectedCol = col;
                    validMoves = getLegalMoves(row, col);
                    drawAll();
                    return;
                }

                selectedRow = null;
                selectedCol = null;
                validMoves = [];
                drawAll();
            }, { passive: false });

            btnRestart.addEventListener('click', resetGame);
            btnUndo.addEventListener('click', function() {
                if (isAiThinking) {
                    // AI正在思考,允许悔棋取消
                    undoMove();
                    return;
                }
                if (gameOver) {
                    undoMove();
                    return;
                }
                if (currentTurn === 'red' && moveHistory.length >= 2) {
                    undoMove();
                } else if (currentTurn === 'red' && moveHistory.length === 1) {
                    undoMove();
                }
            });

            document.addEventListener('keydown', function(e) {
                if (e.ctrlKey && e.key === 'z') {
                    e.preventDefault();
                    if (isAiThinking || gameOver || (currentTurn === 'red' && moveHistory.length >= 1)) {
                        undoMove();
                    }
                }
                if (e.key === 'r' && e.ctrlKey) {
                    e.preventDefault();
                    resetGame();
                }
                if (e.key === 'Escape') {
                    if (!isAiThinking && currentTurn === 'red') {
                        selectedRow = null;
                        selectedCol = null;
                        validMoves = [];
                        drawAll();
                    }
                }
            });

            // ==================== 启动 ====================
            function init() {
                resetGame();
                updateUI();
                drawAll();
            }

            init();

            console.log('🏯 中国象棋 · 人机对弈模式已就绪');
            console.log('  - 你扮演红方(下方),AI扮演黑方(上方)');
            console.log('  - 点击己方棋子选中,再点击目标位置移动');
            console.log('  - 绿色圆点 = 可走位置,红色虚线圈 = 可吃子位置');
            console.log('  - AI使用Alpha-Beta搜索,深度3层');
            console.log('  - Ctrl+Z 或点击"悔棋"撤销(会撤销AI回应+你的上一步)');
            console.log('  - Ctrl+R 重新开始 | Esc 取消选择');
        })();
    </script>
</body>
</html>

国际象棋

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>国际象棋 - 对战AI (修复版)</title>
    <style>
        :root {
            --board-size: min(560px, 90vw);
            --cell-size: calc(var(--board-size) / 8);
            --light-cell: #f0d9b5;
            --dark-cell: #b58863;
            --selected: #7fc97f;
            --legal-move: rgba(0, 0, 0, 0.25);
            --legal-capture: rgba(255, 50, 50, 0.45);
            --last-move: rgba(255, 255, 0, 0.5);
            --check: rgba(255, 60, 60, 0.75);
            --bg: #1a1a2e;
            --panel-bg: #16213e;
            --text: #e0e0e0;
            --accent: #e6b422;
        }

        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            background: var(--bg);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            user-select: none;
            padding: 10px;
        }

        .container {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 16px;
            width: 100%;
            max-width: 700px;
        }

        .board-wrapper {
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 0 30px rgba(230,180,34,0.25), 0 8px 32px rgba(0,0,0,0.5);
            border: 4px solid #3d2b1f;
        }

        .board-container {
            display: grid;
            grid-template-columns: repeat(8, var(--cell-size));
            grid-template-rows: repeat(8, var(--cell-size));
            width: var(--board-size);
            height: var(--board-size);
        }

        .cell {
            width: var(--cell-size);
            height: var(--cell-size);
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            position: relative;
            font-size: calc(var(--cell-size) * 0.78);
            transition: background-color 0.1s;
        }
        .cell.light { background: var(--light-cell); }
        .cell.dark { background: var(--dark-cell); }
        .cell.selected { background: var(--selected) !important; box-shadow: inset 0 0 0 3px rgba(0,0,0,0.4); }
        .cell.last-move { background: var(--last-move) !important; }
        .cell.legal-move::after {
            content: '';
            position: absolute;
            width: 28%;
            height: 28%;
            background: var(--legal-move);
            border-radius: 50%;
            pointer-events: none;
        }
        .cell.legal-capture::after {
            content: '';
            position: absolute;
            width: 85%;
            height: 85%;
            border: 5px solid rgba(220,40,40,0.65);
            border-radius: 50%;
            pointer-events: none;
        }
        .cell.in-check {
            background: var(--check) !important;
            animation: checkPulse 0.6s infinite alternate;
        }
        @keyframes checkPulse {
            from { box-shadow: inset 0 0 8px rgba(255,0,0,0.7); }
            to { box-shadow: inset 0 0 22px rgba(255,0,0,0.95); }
        }

        .piece { pointer-events: none; text-shadow: 0 1px 3px rgba(0,0,0,0.3); }

        .promotion-overlay {
            display: none;
            position: fixed; top:0;left:0; width:100%; height:100%;
            background: rgba(0,0,0,0.7); z-index:100;
            justify-content: center; align-items: center;
        }
        .promotion-overlay.active { display: flex; }
        .promotion-dialog {
            background: #2c2416; border: 3px solid var(--accent);
            border-radius: 16px; padding: 20px;
            display: flex; flex-direction: column; align-items: center; gap: 12px;
        }
        .promotion-options { display: flex; gap: 10px; }
        .promotion-btn {
            width: 60px; height: 60px; font-size: 42px;
            border-radius: 12px; cursor: pointer; border: 2px solid #5a4a30;
            background: #f0d9b5; transition: 0.15s;
            display: flex; align-items: center; justify-content: center;
        }
        .promotion-btn:hover { background: #ffe0a0; transform: scale(1.1); }

        .info-panel { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
        .status {
            background: var(--panel-bg); color: var(--text);
            padding: 8px 18px; border-radius: 25px; font-weight: 600;
            min-width: 160px; text-align: center; border: 1px solid rgba(255,255,255,0.1);
        }
        .status.thinking { color: #4ecdc4; animation: thinkGlow 0.8s infinite alternate; }
        .status.gameover { color: #ff6b6b; }
        @keyframes thinkGlow {
            from { box-shadow: 0 0 8px rgba(78,205,196,0.4); }
            to { box-shadow: 0 0 22px rgba(78,205,196,0.8); }
        }

        .btn {
            padding: 8px 20px; border-radius: 25px; font-weight: 700;
            cursor: pointer; border: 2px solid transparent; font-family: inherit;
            background: var(--accent); color: #1a1a2e; border-color: var(--accent);
        }
        .btn:hover { background: #f0c830; transform: translateY(-1px); }
        .btn:disabled { opacity: 0.4; pointer-events: none; }
        .btn-undo { background: transparent; color: var(--text); border-color: rgba(255,255,255,0.3); }
        .btn-undo:hover { background: rgba(255,255,255,0.1); }
        .captured-area { font-size: 1.2rem; color: #ccc; min-height: 24px; }
    </style>
</head>
<body>
<div class="container">
    <div class="board-wrapper"><div class="board-container" id="board"></div></div>
    <div class="captured-area" id="capturedByWhite"></div>
    <div class="captured-area" id="capturedByBlack"></div>
    <div class="info-panel">
        <button class="btn btn-undo" id="btnUndo">⟲ 撤回</button>
        <div class="status" id="status">轮到你走 · 白方</div>
        <button class="btn" id="btnNewGame">🔄 新游戏</button>
    </div>
</div>
<div class="promotion-overlay" id="promotionOverlay">
    <div class="promotion-dialog">
        <span style="color:#fff">选择升变棋子</span>
        <div class="promotion-options" id="promotionOptions"></div>
    </div>
</div>

<script>
    (function() {
        // ----- 基础定义 -----
        const EMPTY = 0;
        const WP=1, WN=2, WB=3, WR=4, WQ=5, WK=6;
        const BP=7, BN=8, BB=9, BR=10, BQ=11, BK=12;
        const WHITE='white', BLACK='black';
        const PIECE_COLOR = p => p===EMPTY ? null : (p<=6 ? WHITE : BLACK);
        const UNICODE = { [WP]:'♙',[WN]:'♘',[WB]:'♗',[WR]:'♖',[WQ]:'♕',[WK]:'♔',
                          [BP]:'♟',[BN]:'♞',[BB]:'♝',[BR]:'♜',[BQ]:'♛',[BK]:'♚' };
        const VALUE = { [WP]:100,[WN]:320,[WB]:330,[WR]:500,[WQ]:900,[WK]:20000,
                        [BP]:100,[BN]:320,[BB]:330,[BR]:500,[BQ]:900,[BK]:20000 };

        // ----- 游戏状态 -----
        class GameState {
            constructor() { this.reset(); }
            reset() {
                this.board = Array(8).fill().map(()=>Array(8).fill(EMPTY));
                // 黑方 (row 0)
                this.board[0] = [BR,BN,BB,BQ,BK,BB,BN,BR];
                this.board[1] = [BP,BP,BP,BP,BP,BP,BP,BP];
                // 白方 (row 7)
                this.board[6] = [WP,WP,WP,WP,WP,WP,WP,WP];
                this.board[7] = [WR,WN,WB,WQ,WK,WB,WN,WR];
                this.currentPlayer = WHITE;
                this.castling = { wK:true, wQ:true, bK:true, bQ:true };
                this.epTarget = null;
                this.moveHistory = [];
                this.capturedByWhite = [];
                this.capturedByBlack = [];
                this.gameOver = false;
                this.result = null;
                this.lastFrom = null; this.lastTo = null;
                this.inCheck = false;
            }
            clone() {
                const c = new GameState();
                c.board = this.board.map(r => [...r]);
                c.currentPlayer = this.currentPlayer;
                c.castling = {...this.castling};
                c.epTarget = this.epTarget ? {...this.epTarget} : null;
                c.moveHistory = [...this.moveHistory];
                c.capturedByWhite = [...this.capturedByWhite];
                c.capturedByBlack = [...this.capturedByBlack];
                c.gameOver = this.gameOver;
                c.result = this.result;
                c.lastFrom = this.lastFrom; c.lastTo = this.lastTo;
                c.inCheck = this.inCheck;
                return c;
            }
            piece(r,c) { return (r>=0&&r<8&&c>=0&&c<8) ? this.board[r][c] : null; }
            set(r,c,p) { this.board[r][c] = p; }
        }

        // ----- 攻击检测 -----
        function isAttackedBy(gs, row, col, attackerColor) {
            const enemyPawn = attackerColor===WHITE ? WP : BP;
            const enemyKnight = attackerColor===WHITE ? WN : BN;
            const enemyBishop = attackerColor===WHITE ? WB : BB;
            const enemyRook = attackerColor===WHITE ? WR : BR;
            const enemyQueen = attackerColor===WHITE ? WQ : BQ;
            const enemyKing = attackerColor===WHITE ? WK : BK;
            const pawnDir = attackerColor===WHITE ? -1 : 1;
            // 兵的攻击
            const pr = row - pawnDir;
            if(pr>=0&&pr<=7) {
                if(col-1>=0 && gs.piece(pr,col-1)===enemyPawn) return true;
                if(col+1<=7 && gs.piece(pr,col+1)===enemyPawn) return true;
            }
            // 马
            for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
                const r=row+dr, c=col+dc;
                if(r>=0&&r<8&&c>=0&&c<8 && gs.piece(r,c)===enemyKnight) return true;
            }
            // 滑子
            const dirs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
            for(const [dr,dc] of dirs) {
                for(let i=1; i<=7; i++) {
                    const r=row+dr*i, c=col+dc*i;
                    if(r<0||r>7||c<0||c>7) break;
                    const p = gs.piece(r,c);
                    if(p===EMPTY) continue;
                    if(p===enemyBishop && dr!==0 && dc!==0) return true;
                    if(p===enemyRook && (dr===0||dc===0)) return true;
                    if(p===enemyQueen) return true;
                    break;
                }
            }
            // 王
            for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
                const r=row+dr, c=col+dc;
                if(r>=0&&r<8&&c>=0&&c<8 && gs.piece(r,c)===enemyKing) return true;
            }
            return false;
        }

        function findKing(gs, color) {
            const king = color===WHITE ? WK : BK;
            for(let r=0;r<8;r++) for(let c=0;c<8;c++) if(gs.board[r][c]===king) return {row:r,col:c};
            return null;
        }

        function inCheck(gs, color) {
            const k = findKing(gs, color);
            if(!k) return true;
            return isAttackedBy(gs, k.row, k.col, color===WHITE?BLACK:WHITE);
        }

        // ----- 走法生成 (伪合法) -----
        function pseudoMoves(gs, row, col) {
            const piece = gs.piece(row,col);
            if(!piece) return [];
            const color = PIECE_COLOR(piece);
            const isW = color===WHITE;
            const forward = isW ? -1 : 1;
            const startRow = isW ? 6 : 1;
            const promoRow = isW ? 0 : 7;
            const enemy = isW ? BLACK : WHITE;
            const moves = [];

            const add = (tr, tc, cap, special=null) => {
                if(tr<0||tr>7||tc<0||tc>7) return;
                const target = gs.piece(tr,tc);
                if(target===EMPTY || PIECE_COLOR(target)===enemy) {
                    moves.push({fr:row,fc:col,tr,tc,piece,captured:target,special,promotion:null});
                }
            };

            if(piece===WP || piece===BP) {
                const fwd = row+forward;
                if(fwd>=0&&fwd<=7 && gs.piece(fwd,col)===EMPTY) {
                    if(fwd===promoRow) {
                        const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
                        opts.forEach(pp => moves.push({fr:row,fc:col,tr:fwd,tc:col,piece,captured:EMPTY,special:'promo',promotion:pp}));
                    } else {
                        add(fwd, col, EMPTY);
                        if(row===startRow && gs.piece(fwd+forward,col)===EMPTY) add(row+2*forward, col, EMPTY, 'double');
                    }
                }
                for(const dc of [-1,1]) {
                    const capRow = row+forward, capCol = col+dc;
                    if(capRow<0||capRow>7||capCol<0||capCol>7) continue;
                    const t = gs.piece(capRow,capCol);
                    if(t!==EMPTY && PIECE_COLOR(t)===enemy) {
                        if(capRow===promoRow) {
                            const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
                            opts.forEach(pp => moves.push({fr:row,fc:col,tr:capRow,tc:capCol,piece,captured:t,special:'promo',promotion:pp}));
                        } else add(capRow, capCol, t);
                    }
                    if(gs.epTarget && gs.epTarget.row===capRow && gs.epTarget.col===capCol) {
                        moves.push({fr:row,fc:col,tr:capRow,tc:capCol,piece,captured:isW?BP:WP,special:'ep',promotion:null});
                    }
                }
            }
            else if(piece===WN || piece===BN) {
                for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) add(row+dr,col+dc);
            }
            else if(piece===WB || piece===BB) {
                for(const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
                    for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
                }
            }
            else if(piece===WR || piece===BR) {
                for(const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
                    for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
                }
            }
            else if(piece===WQ || piece===BQ) {
                for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
                    for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
                }
            }
            else if(piece===WK || piece===BK) {
                for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) add(row+dr,col+dc);
                // 易位
                if(!inCheck(gs, color)) {
                    const kr = isW?7:0;
                    if(row===kr && col===4) {
                        if(gs.castling[isW?'wK':'bK'] && gs.piece(kr,5)===EMPTY && gs.piece(kr,6)===EMPTY &&
                           gs.piece(kr,7)===(isW?WR:BR) && !isAttackedBy(gs,kr,5,enemy) && !isAttackedBy(gs,kr,6,enemy))
                            moves.push({fr:row,fc:col,tr:kr,tc:6,piece,captured:EMPTY,special:'castleK',promotion:null});
                        if(gs.castling[isW?'wQ':'bQ'] && gs.piece(kr,1)===EMPTY && gs.piece(kr,2)===EMPTY && gs.piece(kr,3)===EMPTY &&
                           gs.piece(kr,0)===(isW?WR:BR) && !isAttackedBy(gs,kr,2,enemy) && !isAttackedBy(gs,kr,3,enemy))
                            moves.push({fr:row,fc:col,tr:kr,tc:2,piece,captured:EMPTY,special:'castleQ',promotion:null});
                    }
                }
            }
            return moves;
        }

        function applyDirect(gs, m) {
            const {fr,fc,tr,tc,piece,special,promotion} = m;
            const isW = PIECE_COLOR(piece)===WHITE;
            if(special==='ep') {
                const capRow = isW ? tr+1 : tr-1;
                gs.set(capRow, tc, EMPTY);
            }
            gs.set(fr, fc, EMPTY);
            gs.set(tr, tc, (special==='promo' && promotion) ? promotion : piece);
            if(special==='castleK') { const kr=isW?7:0; gs.set(kr,7,EMPTY); gs.set(kr,5,isW?WR:BR); }
            if(special==='castleQ') { const kr=isW?7:0; gs.set(kr,0,EMPTY); gs.set(kr,3,isW?WR:BR); }
            // 更新易位权
            if(piece===WK) { gs.castling.wK=gs.castling.wQ=false; }
            if(piece===BK) { gs.castling.bK=gs.castling.bQ=false; }
            if(fr===7&&fc===0||tr===7&&tc===0) gs.castling.wQ=false;
            if(fr===7&&fc===7||tr===7&&tc===7) gs.castling.wK=false;
            if(fr===0&&fc===0||tr===0&&tc===0) gs.castling.bQ=false;
            if(fr===0&&fc===7||tr===0&&tc===7) gs.castling.bK=false;
            gs.epTarget = (special==='double') ? {row: isW?tr+1:tr-1, col:tc} : null;
            gs.currentPlayer = gs.currentPlayer===WHITE ? BLACK : WHITE;
        }

        function legalMoves(gs, row, col) {
            const all = pseudoMoves(gs, row, col);
            const color = PIECE_COLOR(gs.piece(row,col));
            const result = [];
            for(const m of all) {
                const test = gs.clone();
                applyDirect(test, m);
                if(!inCheck(test, color)) result.push(m);
            }
            return result;
        }

        function allLegalMoves(gs, color) {
            const moves = [];
            for(let r=0;r<8;r++) for(let c=0;c<8;c++) {
                if(gs.piece(r,c)!==EMPTY && PIECE_COLOR(gs.piece(r,c))===color)
                    moves.push(...legalMoves(gs, r, c));
            }
            return moves;
        }

        // ----- 执行走法 (完整) -----
        function applyFull(gs, m) {
            const isW = PIECE_COLOR(m.piece)===WHITE;
            let capPiece = m.captured;
            if(m.special==='ep') capPiece = isW ? BP : WP;
            if(capPiece!==EMPTY) {
                if(isW) gs.capturedByWhite.push(capPiece);
                else gs.capturedByBlack.push(capPiece);
            }
            const hist = {
                m: {...m},
                castling: {...gs.castling},
                ep: gs.epTarget ? {...gs.epTarget} : null,
                lastFrom: gs.lastFrom, lastTo: gs.lastTo,
                inCheck: gs.inCheck, capPiece
            };
            applyDirect(gs, m);
            gs.lastFrom = {row:m.fr, col:m.fc};
            gs.lastTo = {row:m.tr, col:m.tc};
            gs.inCheck = inCheck(gs, gs.currentPlayer);
            if(allLegalMoves(gs, gs.currentPlayer).length===0) {
                gs.gameOver = true;
                gs.result = gs.inCheck ? (gs.currentPlayer===WHITE?'black-wins':'white-wins') : 'draw';
            }
            gs.moveHistory.push(hist);
        }

        function undoMove(gs) {
            if(!gs.moveHistory.length) return false;
            const h = gs.moveHistory.pop();
            const m = h.m;
            gs.set(m.tr, m.tc, EMPTY);
            gs.set(m.fr, m.fc, m.piece);
            if(m.special==='ep') {
                const capRow = PIECE_COLOR(m.piece)===WHITE ? m.tr+1 : m.tr-1;
                gs.set(capRow, m.tc, h.capPiece);
            } else if(m.captured!==EMPTY) {
                gs.set(m.tr, m.tc, m.captured);
            }
            if(m.special==='castleK') { const kr=PIECE_COLOR(m.piece)===WHITE?7:0; gs.set(kr,5,EMPTY); gs.set(kr,7,PIECE_COLOR(m.piece)===WHITE?WR:BR); }
            if(m.special==='castleQ') { const kr=PIECE_COLOR(m.piece)===WHITE?7:0; gs.set(kr,3,EMPTY); gs.set(kr,0,PIECE_COLOR(m.piece)===WHITE?WR:BR); }
            gs.castling = h.castling;
            gs.epTarget = h.ep;
            gs.lastFrom = h.lastFrom; gs.lastTo = h.lastTo;
            gs.inCheck = h.inCheck;
            gs.currentPlayer = gs.currentPlayer===WHITE ? BLACK : WHITE;
            gs.gameOver = false; gs.result = null;
            const arr = PIECE_COLOR(m.piece)===WHITE ? gs.capturedByWhite : gs.capturedByBlack;
            const idx = arr.lastIndexOf(h.capPiece);
            if(idx>=0) arr.splice(idx,1);
            return true;
        }

        // ----- AI (深度3) -----
        function evaluate(gs) {
            let score=0;
            for(let r=0;r<8;r++) for(let c=0;c<8;c++) {
                const p=gs.board[r][c]; if(!p) continue;
                const isW = p<=6;
                score += isW ? VALUE[p] : -VALUE[p];
            }
            return score;
        }

        function minimax(gs, depth, alpha, beta, maximizing) {
            if(depth===0 || gs.gameOver) {
                if(gs.gameOver) {
                    if(gs.result==='white-wins') return 99999;
                    if(gs.result==='black-wins') return -99999;
                    return 0;
                }
                return evaluate(gs);
            }
            const color = gs.currentPlayer;
            const moves = allLegalMoves(gs, color);
            if(!moves.length) return color===WHITE ? -99999 : 99999;
            if(color===WHITE) {
                let best=-Infinity;
                for(const m of moves) {
                    const child=gs.clone(); applyDirect(child, m);
                    best = Math.max(best, minimax(child, depth-1, alpha, beta, false));
                    alpha = Math.max(alpha, best);
                    if(beta<=alpha) break;
                }
                return best;
            } else {
                let best=Infinity;
                for(const m of moves) {
                    const child=gs.clone(); applyDirect(child, m);
                    best = Math.min(best, minimax(child, depth-1, alpha, beta, true));
                    beta = Math.min(beta, best);
                    if(beta<=alpha) break;
                }
                return best;
            }
        }

        function bestMove(gs) {
            const moves = allLegalMoves(gs, BLACK);
            if(!moves.length) return null;
            let best = null, bestVal = Infinity;
            for(const m of moves) {
                const child=gs.clone(); applyDirect(child, m);
                const val = minimax(child, 2, -Infinity, Infinity, true);
                if(val < bestVal) { bestVal=val; best=m; }
            }
            return best;
        }

        // ----- UI -----
        const boardEl = document.getElementById('board');
        const statusEl = document.getElementById('status');
        const whiteCapEl = document.getElementById('capturedByWhite');
        const blackCapEl = document.getElementById('capturedByBlack');
        const promoOverlay = document.getElementById('promotionOverlay');
        const promoOpts = document.getElementById('promotionOptions');
        const btnUndo = document.getElementById('btnUndo');
        const btnNew = document.getElementById('btnNewGame');

        let gs = new GameState();
        let selected = null;
        let legals = [];
        let aiThinking = false;
        let pendingPromo = null;

        function render() {
            boardEl.innerHTML = '';
            for(let r=0; r<8; r++) {
                for(let c=0; c<8; c++) {
                    const cell = document.createElement('div');
                    cell.className = 'cell ' + ((r+c)%2===0 ? 'light' : 'dark');
                    if(selected && selected.row===r && selected.col===c) cell.classList.add('selected');
                    if(gs.lastFrom && gs.lastFrom.row===r && gs.lastFrom.col===c) cell.classList.add('last-move');
                    if(gs.lastTo && gs.lastTo.row===r && gs.lastTo.col===c) cell.classList.add('last-move');
                    if(gs.inCheck) {
                        const king = findKing(gs, gs.currentPlayer);
                        if(king && king.row===r && king.col===c) cell.classList.add('in-check');
                    }
                    const isTarget = legals.some(m => m.tr===r && m.tc===c);
                    if(isTarget) {
                        const m = legals.find(m => m.tr===r && m.tc===c);
                        const cap = m && (m.captured!==EMPTY || m.special==='ep');
                        cell.classList.add(cap ? 'legal-capture' : 'legal-move');
                    }
                    const piece = gs.piece(r,c);
                    if(piece) {
                        const span = document.createElement('span');
                        span.className = 'piece';
                        span.textContent = UNICODE[piece];
                        cell.appendChild(span);
                    }
                    cell.addEventListener('click', () => clickCell(r,c));
                    boardEl.appendChild(cell);
                }
            }
        }

        function updateStatus() {
            statusEl.classList.remove('thinking','gameover');
            if(gs.gameOver) {
                statusEl.classList.add('gameover');
                statusEl.textContent = gs.result==='white-wins' ? '🏆 白方胜!' : (gs.result==='black-wins' ? '🏆 AI胜!' : '🤝 平局');
            } else if(aiThinking) {
                statusEl.classList.add('thinking');
                statusEl.textContent = '🤔 AI思考中...';
            } else {
                statusEl.textContent = gs.currentPlayer===WHITE ? (gs.inCheck?'⚠️ 将军!轮到白方':'轮到白方走棋') : '黑方(AI)走棋';
            }
        }

        function updateCaptured() {
            const sortArr = arr => [...arr].sort((a,b)=> (VALUE[b]||0)-(VALUE[a]||0));
            whiteCapEl.textContent = '白方吃: ' + (gs.capturedByWhite.length ? sortArr(gs.capturedByWhite).map(p=>UNICODE[p]).join(' ') : '---');
            blackCapEl.textContent = '黑方吃: ' + (gs.capturedByBlack.length ? sortArr(gs.capturedByBlack).map(p=>UNICODE[p]).join(' ') : '---');
        }

        function refresh() { render(); updateStatus(); updateCaptured(); btnUndo.disabled = aiThinking || gs.moveHistory.length===0; }

        function clickCell(r,c) {
            if(aiThinking || gs.gameOver || gs.currentPlayer!==WHITE) return;
            if(selected && legals.some(m=>m.tr===r&&m.tc===c)) {
                const move = legals.find(m=>m.tr===r&&m.tc===c);
                if(move) return executeMove(move);
            }
            const piece = gs.piece(r,c);
            if(piece && PIECE_COLOR(piece)===WHITE) {
                selected = {row:r, col:c};
                legals = legalMoves(gs, r, c);
                if(legals.length===0) { selected=null; legals=[]; } // 无合法走法自动取消
                return refresh();
            }
            selected = null; legals = [];
            refresh();
        }

        function executeMove(m) {
            if(m.special==='promo' && !m.promotion) {
                pendingPromo = m;
                showPromoDialog(m);
                return;
            }
            finalizeMove(m);
        }

        function finalizeMove(m) {
            applyFull(gs, m);
            selected = null; legals = [];
            refresh();
            if(!gs.gameOver) setTimeout(aiTurn, 100);
        }

        function showPromoDialog(m) {
            const isW = PIECE_COLOR(m.piece)===WHITE;
            const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
            promoOpts.innerHTML = '';
            opts.forEach(pp => {
                const btn = document.createElement('button');
                btn.className = 'promotion-btn';
                btn.textContent = UNICODE[pp];
                btn.onclick = () => { m.promotion = pp; promoOverlay.classList.remove('active'); pendingPromo=null; finalizeMove(m); };
                promoOpts.appendChild(btn);
            });
            promoOverlay.classList.add('active');
        }

        function aiTurn() {
            if(gs.gameOver || gs.currentPlayer!==BLACK) return;
            aiThinking = true; refresh();
            setTimeout(() => {
                const move = bestMove(gs);
                if(move) {
                    if(move.special==='promo' && !move.promotion) move.promotion = BQ;
                    applyFull(gs, move);
                }
                aiThinking = false;
                selected = null; legals = [];
                refresh();
            }, 50);
        }

        btnNew.onclick = () => {
            gs.reset(); selected=null; legals=[]; aiThinking=false; pendingPromo=null;
            promoOverlay.classList.remove('active'); refresh();
        };
        btnUndo.onclick = () => {
            if(aiThinking) return;
            if(gs.currentPlayer===WHITE && gs.moveHistory.length>=2) { undoMove(gs); undoMove(gs); }
            else if(gs.currentPlayer===BLACK && gs.moveHistory.length>=1) { undoMove(gs); }
            selected=null; legals=[]; refresh();
        };
        document.addEventListener('keydown', e => {
            if(e.key==='Escape') { selected=null; legals=[]; promoOverlay.classList.remove('active'); pendingPromo=null; refresh(); }
        });

        gs.reset(); refresh();
    })();
</script>
</body>
</html>
相关推荐
之歆5 小时前
DAY04_HTML&CSS_从表单到样式的完整学习指南
css·html·产品运营
2401_8784545312 小时前
html和css的复习(1)
前端·css·html
冰的第三次元13 小时前
一天通关HTML80%核心细节(新手友好版)
html
米丘19 小时前
vue3.5.x 单文件组件(SFC)样式编译过程
css·vue.js·postcss
dsyyyyy110119 小时前
HTML总结
前端·html
techdashen21 小时前
Cloudflare HTML 解析器的十年演化史(一)
前端·html
ZC跨境爬虫1 天前
前端实战复盘:从零完成Apple中国大陆官网UI第一阶段全量静态复刻
前端·css·ui·html
琪露诺大湿1 天前
网页聊天系统——测试报告
java·软件测试·功能测试·websocket·html·项目·测试报告
a1117761 天前
RIPPLE 流体交互(html 开源)
前端·javascript·html