五子棋终于战胜AI了!

五子棋终于战胜AI了!
五子棋终于战胜AI了!

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>五子棋 AI 对战</title>
    <style>
        :root {
            --bg-color: #f5f6fa;
            --board-color: #e4b980;
            --line-color: #634d31;
            --primary-color: #4a90e2;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none;
            -webkit-user-select: none;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background-color: var(--bg-color);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            padding: 10px;
        }

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

        h1 {
            font-size: 1.5rem;
            color: #333;
            font-weight: 600;
        }

        .status {
            font-size: 1.1rem;
            font-weight: bold;
            color: var(--primary-color);
            height: 24px;
        }

        .board-wrapper {
            width: 100%;
            aspect-ratio: 1 / 1;
            background-color: var(--board-color);
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
            padding: 12px;
            position: relative;
        }

        canvas {
            width: 100%;
            height: 100%;
            display: block;
            cursor: pointer;
        }

        .btn {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 10px 24px;
            font-size: 1rem;
            border-radius: 20px;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
            transition: all 0.2s ease;
        }

        .btn:active {
            transform: scale(0.95);
            box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
        }
    </style>
</head>
<body>

<div class="container">
    <h1>五子棋 AI 对战</h1>
    <div class="status" id="status-text">你是黑棋,请落子</div>
    
    <div class="board-wrapper">
        <canvas id="gobang"></canvas>
    </div>

    <button class="btn" onclick="restartGame()">重新开始</button>
</div>

<script>
    const canvas = document.getElementById('gobang');
    const ctx = canvas.getContext('2d');
    const statusText = document.getElementById('status-text');

    const GRID_SIZE = 15; 
    let cellSize = 0;
    // ===== 修复点 1:直接初始化为 15×15 的二维零矩阵 =====
    let board = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0));
    let gameOver = false;
    let isAiTurn = false;
    let lastMove = null; 
    let haloAngle = 0;

    function initCanvas() {
        const rect = canvas.getBoundingClientRect();
        const dpr = window.devicePixelRatio || 1;
        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        ctx.scale(dpr, dpr);
        cellSize = rect.width / (GRID_SIZE + 1);
        // ===== 修复点 2:移除这里的 render(),避免在 board 未就绪时渲染 =====
        // 渲染工作交给 restartGame() 或动画循环
    }

    function restartGame() {
        board = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0));
        gameOver = false;
        isAiTurn = false;
        lastMove = null;
        statusText.innerText = "你是黑棋,请落子";
        statusText.style.color = "#4a90e2";
        render();
    }

    // 每一帧动画都重绘整个棋盘和现有棋子,确保落子持续显示
    function render() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // 1. 绘制网格
        ctx.strokeStyle = '#634d31';
        ctx.lineWidth = 1;
        for (let i = 0; i < GRID_SIZE; i++) {
            ctx.beginPath();
            ctx.moveTo(cellSize, cellSize * (i + 1));
            ctx.lineTo(cellSize * GRID_SIZE, cellSize * (i + 1));
            ctx.stroke();
            
            ctx.beginPath();
            ctx.moveTo(cellSize * (i + 1), cellSize);
            ctx.lineTo(cellSize * (i + 1), cellSize * GRID_SIZE);
            ctx.stroke();
        }

        // 2. 绘制星位
        const stars = [[3, 3], [11, 3], [7, 7], [3, 11], [11, 11]];
        ctx.fillStyle = '#634d31';
        stars.forEach(([x, y]) => {
            ctx.beginPath();
            ctx.arc(cellSize * (x + 1), cellSize * (y + 1), 4, 0, Math.PI * 2);
            ctx.fill();
        });

        // 3. 稳固绘制所有棋子
        for (let x = 0; x < GRID_SIZE; x++) {
            for (let y = 0; y < GRID_SIZE; y++) {
                if (board[x][y] !== 0) {
                    drawPiece(x, y, board[x][y]);
                }
            }
        }

        // 4. 叠加最新的浮动光环
        if (lastMove) {
            drawLastMoveHalo(lastMove.x, lastMove.y);
        }
    }

    function drawPiece(x, y, type) {
        const cx = cellSize * (x + 1);
        const cy = cellSize * (y + 1);
        const radius = cellSize * 0.43;

        ctx.save();
        ctx.beginPath();
        ctx.arc(cx, cy, radius, 0, Math.PI * 2);

        const gradient = ctx.createRadialGradient(cx - radius*0.15, cy - radius*0.15, radius * 0.1, cx, cy, radius);
        if (type === 1) {
            gradient.addColorStop(0, '#666');
            gradient.addColorStop(1, '#000');
        } else {
            gradient.addColorStop(0, '#fff');
            gradient.addColorStop(0.8, '#ddd');
            gradient.addColorStop(1, '#bbb');
        }
        
        ctx.fillStyle = gradient;
        ctx.shadowBlur = 4;
        ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
        ctx.shadowOffsetX = 1;
        ctx.shadowOffsetY = 2;
        ctx.fill();
        ctx.restore();
    }

    function drawLastMoveHalo(x, y) {
        const cx = cellSize * (x + 1);
        const cy = cellSize * (y + 1);
        const baseRadius = cellSize * 0.43;
        
        const pulse = Math.sin(haloAngle) * 3; 
        const haloRadius = baseRadius + 3 + pulse;
        const opacity = 0.5 - (pulse + 3) * 0.04;

        ctx.save();
        ctx.beginPath();
        ctx.arc(cx, cy, haloRadius, 0, Math.PI * 2);
        ctx.strokeStyle = `rgba(74, 144, 226, ${Math.max(0.1, opacity)})`;
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.restore();
    }

    function animate() {
        haloAngle += 0.07;
        render();
        requestAnimationFrame(animate);
    }

    canvas.addEventListener('click', function(e) {
        if (gameOver || isAiTurn) return;

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

        const x = Math.round(clientX / cellSize) - 1;
        const y = Math.round(clientY / cellSize) - 1;

        if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE || board[x][y] !== 0) return;

        board[x][y] = 1;
        lastMove = { x, y };
        render();

        if (checkWin(x, y, 1)) {
            statusText.innerText = "恭喜,你赢了!🎉";
            statusText.style.color = "#2ecc71";
            gameOver = true;
            return;
        }

        isAiTurn = true;
        statusText.innerText = "AI 正在思考...";
        statusText.style.color = "#e67e22";

        setTimeout(aiMove, 300);
    });

    function aiMove() {
        if (gameOver) return;

        let bestScore = -1;
        let bestPoints = [];

        for (let x = 0; x < GRID_SIZE; x++) {
            for (let y = 0; y < GRID_SIZE; y++) {
                if (board[x][y] === 0) {
                    let aiScore = evaluatePoint(x, y, 2);
                    let playerScore = evaluatePoint(x, y, 1);
                    let totalScore = aiScore + playerScore * 0.9; 

                    if (totalScore > bestScore) {
                        bestScore = totalScore;
                        bestPoints = [{x, y}];
                    } else if (totalScore === bestScore) {
                        bestPoints.push({x, y});
                    }
                }
            }
        }

        if (bestPoints.length === 0) {
            statusText.innerText = "平局!";
            gameOver = true;
            return;
        }

        const move = bestPoints[Math.floor(Math.random() * bestPoints.length)];
        board[move.x][move.y] = 2;
        lastMove = { x: move.x, y: move.y };
        render();

        if (checkWin(move.x, move.y, 2)) {
            statusText.innerText = "AI 赢了,再接再厉!";
            statusText.style.color = "#e74c3c";
            gameOver = true;
            return;
        }

        isAiTurn = false;
        statusText.innerText = "你是黑棋,请落子";
        statusText.style.color = "#4a90e2";
    }

    function evaluatePoint(x, y, type) {
        let score = 0;
        const directions = [[1,0], [0,1], [1,1], [1,-1]];

        directions.forEach(([dx, dy]) => {
            let count = 1;
            let block1 = false;
            let block2 = false;

            let tx = x + dx, ty = y + dy;
            while(tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE) {
                if (board[tx][ty] === type) {
                    count++;
                } else {
                    if (board[tx][ty] !== 0) block1 = true;
                    break;
                }
                tx += dx; ty += dy;
            }
            if (tx < 0 || tx >= GRID_SIZE || ty < 0 || ty >= GRID_SIZE) block1 = true;

            tx = x - dx; ty = y - dy;
            while(tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE) {
                if (board[tx][ty] === type) {
                    count++;
                } else {
                    if (board[tx][ty] !== 0) block2 = true;
                    break;
                }
                tx -= dx; ty -= dy;
            }
            if (tx < 0 || tx >= GRID_SIZE || ty < 0 || ty >= GRID_SIZE) block2 = true;

            if (count >= 5) score += 100000;
            else if (count === 4) {
                if (!block1 && !block2) score += 10000;
                else if (!block1 || !block2) score += 1000;
            } else if (count === 3) {
                if (!block1 && !block2) score += 1000;
                else if (!block1 || !block2) score += 100;
            } else if (count === 2) {
                if (!block1 && !block2) score += 100;
                else if (!block1 || !block2) score += 10;
            }
        });

        return score;
    }

    function checkWin(x, y, type) {
        const directions = [[1,0], [0,1], [1,1], [1,-1]];
        for (let [dx, dy] of directions) {
            let count = 1;
            let tx = x + dx, ty = y + dy;
            while (tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE && board[tx][ty] === type) {
                count++; tx += dx; ty += dy;
            }
            tx = x - dx; ty = y - dy;
            while (tx >= 0 && tx < GRID_SIZE && ty >= 0 && ty < GRID_SIZE && board[tx][ty] === type) {
                count++; tx -= dx; ty -= dy;
            }
            if (count >= 5) return true;
        }
        return false;
    }

    window.addEventListener('resize', initCanvas);
    window.onload = () => {
        // 先初始化画布(计算 cellSize 等)
        initCanvas();
        // 再重置游戏(初始化 board 并首次渲染)
        restartGame();
        // 启动动画循环
        animate();
    };
</script>

</body>
</html>

改编自 @518 佬发的源码,想玩的佬友也可以试试哈

4 个帖子 - 4 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文