【canvas】JS五子棋UI與AI

注:本文思路源於幕客網JS實現人機大戰五子棋

0. 效果

在這裏插入圖片描述

1. 代碼

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Chess</title>
</head>

<body>
    <style>
        canvas {
            display: block;
            margin: 5px auto;
            box-shadow: -2px -2px 2px #efefef, 5px 5px 5px #b9b9b9;
        }

        .information {
            text-align: center;
        }

        button {
            background: dodgerblue;
            color: white;
            border: 0;
        }
    </style>
    <canvas id="chess" width="450px" height="450px"></canvas>
    <div class="information">
        <div class="operator">
            <button id="order" onclick="changeOrder()">先後</button>
            <button id="mode" onclick="changeMode()">模式</button>
            <button id="replay" onclick="replay()">重玩</button>
        </div>
        <div id="records">勝 0 負 0</div>
    </div>
    <script>
        /* 遊戲數據配置 */
        const size = 450; // 棋盤尺寸
        const lineCounts = 15; // 棋盤行數
        const rowWidth = size / lineCounts; // 棋盤行寬
        const margin = rowWidth / 2; // 棋盤邊界
        const pieceRadius = rowWidth / 2; // 棋子半徑
        const offset = 2; // 漸變偏移
        const conJunctions = 5; // 五子棋,五子連珠
        var order = true; // 黑先; 白後
        var mode = false; // Player Vs Player; Player Vs Computer;

        var player = order;
        var ord = {
            true: "先手執黑",
            false: "後手執白"
        }
        var mod = {
            true: "人人對戰",
            false: "人機對戰"
        }
        var records = {
            win: 0,
            lose: 0
        };
        // winMatrix;
        var winMatrix = [];
        var counts = 0;
        // black and white
        var black = [];
        var white = [];
        // pieceMatrix;
        var pieceMatrix = [];
        // 棋盤,canvas繪製棋盤和棋子
        var chess = document.getElementById("chess");
        var context = chess.getContext("2d");

        /* ----- */
        document.getElementById("order").innerText = ord[order];
        document.getElementById("mode").innerText = mod[mode];
        start();
        /* ----- */

        function changeOrder() {
            order = !order;
            document.getElementById("order").innerText = ord[order];
            start();
        }

        function changeMode() {
            mode = !mode;
            // 清零記錄
            records = {
                win: 0,
                lose: 0
            };
            updateRecords();
            if (mode) { // 人人對戰不計分
                document.getElementById("records").style.display = "none";
            } else {
                document.getElementById("records").style.display = "block";
            }
            document.getElementById("mode").innerText = mod[mode];
            start();
        }

        function initData() {
            winMatrix = [];
            counts = 0;
            black = [];
            white = [];
            pieceMatrix = [];

            // 初始化
            for (let i = 0; i < lineCounts; i++) {
                winMatrix[i] = [];
                for (let j = 0; j < lineCounts; j++) {
                    winMatrix[i][j] = [];
                }
            }

            // 橫向
            for (let i = 0; i < lineCounts; i++) {
                for (let j = 0; j <= lineCounts - conJunctions; j++) {
                    // 選擇到合適的起始點,加5次即可
                    for (let k = 0; k < conJunctions; k++) {
                        winMatrix[i][j + k][counts] = true;
                    }
                    counts++;
                }
            }

            // 豎向
            for (let i = 0; i <= lineCounts - conJunctions; i++) {
                for (let j = 0; j < lineCounts; j++) {
                    // 選擇到合適的起始點,加5次即可
                    for (let k = 0; k < conJunctions; k++) {
                        winMatrix[i + k][j][counts] = true;
                    }
                    counts++;
                }
            }

            // 正斜向[\]
            for (let i = 0; i <= lineCounts - conJunctions; i++) {
                for (let j = 0; j <= lineCounts - conJunctions; j++) {
                    // 選擇到合適的起始點,加5次即可
                    for (let k = 0; k < conJunctions; k++) {
                        winMatrix[i + k][j + k][counts] = true;
                    }
                    counts++;
                }
            }

            // 反斜向[/]
            for (let i = 0; i <= lineCounts - conJunctions; i++) {
                for (let j = conJunctions - 1; j < lineCounts; j++) {
                    // 選擇到合適的起始點,加5次即可
                    for (let k = 0; k < conJunctions; k++) {
                        winMatrix[i + k][j - k][counts] = true;
                    }
                    counts++;
                }
            }

            // 黑白棋贏法統計
            for (let i = 0; i < counts; i++) {
                black[i] = 0;
                white[i] = 0;
            }

            // 落子矩陣
            for (let i = 0; i < lineCounts; i++) {
                pieceMatrix[i] = [];
                for (let j = 0; j < lineCounts; j++) {
                    pieceMatrix[i][j] = true;
                }
            }
        }

        // 開始遊戲
        function start() {
            // 初始化
            initData();
            // 繪盤
            drawChessBoard();
            // 電腦先手隨機內部落子
            if (!mode && !order) { // 人機且電腦先手
                let x = Math.floor(Math.random() * conJunctions) + conJunctions;
                let y = Math.floor(Math.random() * conJunctions) + conJunctions;
                oneStep(x, y, true);
                player = false; // 白
            }
            // 人落子
            chess.addEventListener("click", clickChessBoard);
        }

        // 重玩
        function replay() {
            // 清盤
            context.clearRect(0, 0, size, size);
            start();
        }

        // 繪製棋盤
        function drawChessBoard() {
            // 背景色
            context.fillStyle = "#CDAA7D";
            context.fillRect(0, 0, size, size);
            // 棋盤線條色
            context.strokeStyle = "#bfbfbf";
            // 劃線
            context.beginPath();
            for (let i = 0; i < lineCounts; i++) {
                let position = margin + i * rowWidth;
                // row lines
                context.moveTo(margin, position);
                context.lineTo(size - margin, position);
                context.stroke();
                // column lines
                context.moveTo(position, margin);
                context.lineTo(position, size - margin);
                context.stroke();
            }
            context.closePath();
        }

        // 繪製棋子
        function drawPiece(i, j, side) {
            let x = margin + i * rowWidth;
            let y = margin + j * rowWidth;
            // arc
            context.beginPath();
            context.arc(x, y, pieceRadius, 0, 2 * Math.PI);
            // gradient
            let gradient = context.createRadialGradient(
                x + offset, y - offset, pieceRadius, x + offset, y - offset, 0);
            if (side) { // 黑
                gradient.addColorStop(0, "#0a0a0a");
                gradient.addColorStop(1, "#636766");
            } else { // 白
                gradient.addColorStop(0, "#d1d1d1");
                gradient.addColorStop(1, "#f9f9f9");
            }
            context.fillStyle = gradient;
            context.fill();
            context.closePath();
        }

        // 遊戲結束
        function gameOver(side) {
            let str = side ? "黑" : "白";
            if (order == side) { // 先手執黑勝 或者 後手執白勝
                records.win++;
            } else {
                records.lose++;
            }
            updateRecords();
            // 設置字體
            context.font = "4em bold 黑體";
            // 設置水平對齊方式
            context.textAlign = "center";
            // 設置垂直對齊方式
            context.textBaseline = "middle";
            // 設置顏色
            context.strokeStyle = "red";
            context.fillStyle = "green";
            // 繪製文字(參數:要寫的字,x座標,y座標)
            context.strokeText(str + " 勝!", size / 2, size / 2);
            context.fillText(str + " 勝!", size / 2, size / 2);
            forbidClick();
        }

        // 更新記錄
        function updateRecords() {
            document.getElementById("records").innerText = `勝 ${records.win}${records.lose}`;
        }

        // 禁止落子,移除事件監聽
        function forbidClick() {
            chess.removeEventListener("click", clickChessBoard);
        }

        // 落子
        function oneStep(i, j, side) {
            // 落子
            drawPiece(i, j, side);
            // 更新棋子矩陣
            pieceMatrix[i][j] = false;
            // 勝負判斷邏輯
            for (let k = 0; k < counts; k++) {
                if (winMatrix[i][j][k]) {
                    if (side) { // 黑
                        white[k] = 6; // 被黑棋落子的贏法,白棋永遠不可能贏了
                        if (++black[k] == 5) {
                            gameOver(true);
                            return false;
                        };
                    } else { // 白
                        black[k] = 6;
                        if (++white[k] == 5) {
                            gameOver(false);
                            return false;
                        };
                    }
                }
            }
            return true;
        }

        // 點擊棋盤事件
        function clickChessBoard(e) {
            let x = e.offsetX; // 相對於chess的偏移
            let y = e.offsetY;
            // 尋找點擊範圍
            let i = Math.floor(x / rowWidth);
            let j = Math.floor(y / rowWidth);
            // 有子不得落子
            if (pieceMatrix[i][j]) {
                // 落子未終局(沒有一方勝利結束遊戲)
                if (oneStep(i, j, player)) {
                    // 黑白交替走子
                    if (mode) {
                        player = !player;
                    } else {
                        computerAI(player);
                    }
                }
            }
        }
        /**
         * 計算機AI實現
         */
        // 攻防策略分數定義
        function deffence(num) {
            let pts = {
                0: 0,
                1: 200,
                2: 400,
                3: 2000,
                4: 10000
            }
            if (num > 4) {
                return 0;
            } else {
                return pts[num];
            }
        }
        function attack(num) {
            let pts = {
                0: 0,
                1: 210,
                2: 420,
                3: 2100,
                4: 20000
            }
            if (num > 4) {
                return 0;
            } else {
                return pts[num];
            }
        }

        // AI
        function computerAI(player) {
            // 判斷黑白方
            let opponent = black, self = white;
            if (!player) {
                opponent = white;
                self = black;
            }
            // 得分數組
            let person = [];
            let com = [];
            // 初始化
            for (let i = 0; i < lineCounts; i++) {
                person[i] = [];
                com[i] = [];
                for (let j = 0; j < lineCounts; j++) {
                    person[i][j] = 0;
                    com[i][j] = 0;
                }
            }
            // 判斷各個落點得分
            for (let i = 0; i < lineCounts; i++) {
                for (let j = 0; j < lineCounts; j++) {
                    if (pieceMatrix[i][j]) { // 可落子
                        for (let k = 0; k < counts; k++) {
                            if (winMatrix[i][j][k]) { // 可贏
                                person[i][j] += deffence(opponent[k]); // 防守
                                com[i][j] += attack(self[k]); // 進攻
                            }
                        }
                    }
                }
            }
            // 尋找最優點
            var max = 0; // 最高分
            var u = 0, v = 0; // 最優落子點
            for (let i = 0; i < lineCounts; i++) {
                for (let j = 0; j < lineCounts; j++) {
                    // 防守最高分
                    if (person[i][j] > max) {
                        max = person[i][j];
                        u = i;
                        v = j;
                    }
                    // 進攻最高分
                    if (com[i][j] > max) {
                        max = com[i][j];
                        u = i;
                        v = j;
                    }
                }
            }
            // 電腦落子
            oneStep(u, v, !player);
        }
    </script>
</body>
</html>

2. 後記

  • clearRect() 清除畫布
  • fill stroke 填充和描邊
  • createRadialGradient(x1,y1,x2,y2) addColorStop(0, color) 徑向漸變
  • beginPath() closePath 開閉路徑
發佈了84 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章