在上次框架改版、引入pinpa的基础上,这次引入了AI下棋功能,这功能主要来自:https://github.com/lihongxun945/gobang   AI功能部分未进行任何变动,直接改了几个接口,直接引入即可。

本次设计代码较多,仍原来几个代码

本次主要重点引入的代码

minmax.worker.js,主要改自开源的代码,由react进行了微调,其中AI部分,直接从开源处将AI文件夹复制过来即可

import Board from './ai/board';
import {minmax} from './ai/minmax';
import {DEFAULT_BOARD_SIZE} from '@/stores/status';

// @ts-ignore
// onmessage = function (event) {
//   const { action, payload } = event.data;
//   console.log(event.data)
//   let res = null;
//   switch (action) {
//     case 'start':
//       res = start(payload.board_size, payload.aiFirst, payload.depth);
//       break;
//     case 'move':
//       res = move(payload.position, payload.depth);
//       break;
//     case 'undo':
//       res = undo();
//       break;
//     case 'end':
//       res = end();
//       break;
//     default:
//       break;
//   }
//   postMessage({
//     action,
//     payload: res,
//   });
// };

let board = new Board(DEFAULT_BOARD_SIZE);
let score = 0, bestPath = [], currentDepth = 0;

const getBoardData = () => {
    return {
        board: JSON.parse(JSON.stringify(board.board)),
        winner: board.getWinner(),
        current_player: board.role,
        history: JSON.parse(JSON.stringify(board.history)),
        size: board.size,
        score,
        bestPath,
        currentDepth,
    }
}

export const start = (board_size, aiFirst = true, depth = 4) => {
    console.log('start', board_size, aiFirst, depth);
    board = new Board(board_size);
    try {
        if (aiFirst) {
            const res = minmax(board, board.role, depth);
            console.log("=====res=====" + res)
            let move;
            [score, move, bestPath, currentDepth] = res;
            console.log("=========================")
            console.log(score)
            console.log(move)
            console.log(bestPath)
            console.log(currentDepth)
            console.log("=========================")
            board.put(move[0], move[1]);
            return [move[0], move[1], board.role]
        }
    } catch (e) {
        console.log(e);
    }
    // return getBoardData();
    return null
};

export const move = (position, depth) => {
    console.log("=====move====" + position)
    try {
        board.put(position[0], position[1]);
    } catch (e) {
        console.log(e);
    }
    if (!board.isGameOver()) {
        const res = minmax(board, board.role, depth);
        let move;
        [score, move, bestPath, currentDepth] = res;
        board.put(move[0], move[1]);
        return [move[0], move[1], board.role]
    }
    // return getBoardData();
};
//
// export const end = () => {
//   console.log("=====end====")
//   // do nothing
//   return getBoardData();
// };
//
// export const undo = () => {
//   console.log("=====undo====")
//   board.undo();
//   board.undo();
//   return getBoardData();
// }

status.js

//默认宽度
export const DEFAULT_BOARD_WIDTH = 375
//默认线条数
export const DEFAULT_BOARD_SIZE = 15
//默认间隙
export const DEFAULT_BOARD_SPACE = 25

export const GameStatus = {
    //就绪状态
    IDLE: "idle",
    // 游戏中
    GAMING: 'gaming',
    // 已获胜
    WINNING: 'winning',
    // 重新开始
    RESTART: 'restart',
}
//黑棋子
export const PLAYER_BLACK = -1
//白棋子
export const PLAYER_WHITE = 1
export const PlayerStatus = {
    // 黑棋
    BLACK: PLAYER_BLACK,
    // 白棋
    WHITE: PLAYER_WHITE,
}

chess.js  状态管理代码

import {defineStore, acceptHMRUpdate} from 'pinia'
import {
    PlayerStatus,
    GameStatus,
    DEFAULT_BOARD_WIDTH,
    DEFAULT_BOARD_SIZE,
    DEFAULT_BOARD_SPACE
} from "@/stores/status.js";

export const useChessStore = defineStore({
    id: 'chess',
    state: () => ({
        status: GameStatus.IDLE,
        boardDetail: {
            width: DEFAULT_BOARD_WIDTH,//棋盘大小
            size: DEFAULT_BOARD_SIZE,//棋盘线数
            space: DEFAULT_BOARD_SPACE,//间隙
        },
        current_player: PlayerStatus.BLACK,//默认黑子(-1)
        board: [],//棋盘内容--Board包含
        history: [],//--Board包含
        size: '',//--Board包含
        aiFirst: true,//AI先行
        depth: 4, // 搜索深度
        currentDepth: 0,
        path: [],
        winner: '',
        score: '',
        bestPath: ''
    }),
    actions: {
        /**
         * 初始化选手(黑)
         */
        initPayer(){
            this.current_player =PlayerStatus.BLACK
            console.log("==action:初始化选手(黑)==初始化选手(黑)==,新:" + this.current_player)
        },
        /**
         * 转换选手
         */
        reversePlayer() {
            this.current_player *= -1
            console.log("==action:reversePlayer==交换选手==,新:" + this.current_player)
        },
        /**
         * 改变选手
         * @param s
         */
        changePlayer(s) {
            switch (s) {
                case PlayerStatus.BLACK:
                    this.current_player = PlayerStatus.BLACK;
                    break
                case PlayerStatus.WHITE:
                    this.current_player = PlayerStatus.WHITE;
                    break;
                default:
            }
            console.log("==action:changePlayer==改变选手==,新:" + s)
        },
        /**
         * 改变游戏状态
         * @param s
         */
        changeGameStatus(s) {
            switch (s) {
                case GameStatus.IDLE:
                    this.status = GameStatus.IDLE;
                    break
                case GameStatus.GAMING:
                    this.status = GameStatus.GAMING;
                    break
                case GameStatus.WINNING:
                    this.status = GameStatus.WINNING;
                    break
                case GameStatus.RESTART:
                    this.status = GameStatus.RESTART;
                    break
                default:
            }
            console.log("==action:changeGameStatus==改变游戏状态==" + s)
        },
        /**
         * 改变棋盘大小
         * @param w
         * @param n
         * @param s
         */
        changeBoardDetail(w, n, s) {
            this.boardDetail = {
                width: w,//棋盘大小
                size: n,//棋盘线数
                space: s,//间隙
            }
            console.log("==action:changeBoardDetail==改变棋盘大小==" + this.boardDetail)
        },
        /**
         * 初始化棋盘内容
         */
        initBoard() {
            const size = this.boardDetail.size
            this.board = Array(size).fill().map(() => Array(size).fill(0));
            console.log("==action:initBoard==初始化棋盘内容==\n" + this.board)

        },
        /**
         * 修改棋盘内容
         * @param board
         */
        changeBoard(board) {
            this.board = board
            console.log("==action:changeBoard==修改棋盘内容==\n" + this.board)
        },
        /**
         * 清空棋盘内容
         */
        clearBoard() {
            const size = this.boardDetail.size
            this.board = Array(size).fill().map(() => Array(size).fill(0));
            console.log("==action:clearBoard==清空棋盘内容==" + this.board)
        },
        /**
         * 清空历史内容
         */
        clearHistory() {
            this.history = []
            console.log("==action:clearBoard==清空历史内容==" + this.history)
        }
    }

})

if (import.meta.hot) {
    import.meta.hot.accept(acceptHMRUpdate(useChessStore, import.meta.hot))
}

GoBang.vue  此部分代码和上次无太大差异

<script>
import MainBoard from './MainBoard.vue'
import OperateBoard from './OperateBoard.vue'

export default {
    components: {
        MainBoard, OperateBoard
    },
    created() {
        document.title = "五子棋";
    }
}
</script>
<template>
    <div class="root-board">
        <MainBoard/>
        <OperateBoard/>
    </div>
</template>

<style scoped>
.root-board {
    margin: 0 auto;
}
</style>

 

MainBoard  ci此部分整体框架和上次类似,功能描述上有调整

<script>
import {useChessStore} from "@/stores/chess.js";
import {GameStatus, PlayerStatus} from "@/stores/status.js";
import {move, start} from "@/components/gobang/minmax.worker.js";

export default {
    setup() {
        //加载store,便于全局使用
        const chessStore = useChessStore()
        return {
            chessStore,
        }
    },
    created() {
        //监视状态变化,以便监听【重置】状态
        this.chessStore.$subscribe((mutation, state) => {
            // console.log(mutation)
            //TODO 生产环境有问题,去掉了mutation.events.key === "status"
            if (state.status === GameStatus.RESTART) {
                //检测到状态位RESTART,需要重置游戏
                this.restartGame()
            }
        })
    },

    // 实例创建完成,仅在首次加载时完成
    mounted() {
        let boardDetail = this.chessStore.boardDetail
        //TODO 清空基础数据,后续测试必要性
        this.initGame()
        this.drawBoard(boardDetail.width,
            boardDetail.size,
            boardDetail.space);
    }
    ,
    methods: {
        /**
         * 初始化游戏参数
         */
        initGame() {
            //清空基础数据并初始化
            this.chessStore.initPayer()
            this.chessStore.initBoard()
            this.chessStore.clearHistory()
            this.chessStore.changeGameStatus(GameStatus.IDLE)
        },
        /**
         * 点击落子
         * @param e
         */
        handleDropChess(e) {
            //存在输赢以后或未开始前,不允许在落子
            if (this.chessStore.status !== GameStatus.GAMING) {
                return;
            }
            const space = this.chessStore.boardDetail.space
            const size = this.chessStore.boardDetail.size
            // 计算棋子落在哪个方格中,并绘制棋子
            const cellX = Math.floor((e.offsetX) / space);
            const cellY = Math.floor((e.offsetY) / space);
            this.drawChess(cellX, cellY)
            //判断输赢
            let winner = this.checkWinner(this.chessStore.board, size)
            if (winner !== null) {
                //代表此次操作有胜负,更新结果
                this.chessStore.changeGameStatus(GameStatus.WINNING)
                alert(winner)
                return;
            }
            //交换选手
            this.chessStore.reversePlayer()
            //交由AI下棋
            this.handleAIDrop()
        },
        /**
         * AI下棋
         */
        handleAIDrop() {
            //接下来交给AI出来,前面均由人为点击产生
            const history = this.chessStore.history
            const h = history[history.length - 1]
            // console.log("x=" + h.x + ' y=' + h.y)
            const res = move([h.x, h.y], this.chessStore.depth)
            this.drawChess(res[0], res[1])
            // console.log(this.chessStore.board)
            console.log("==================AI下子结束=============")

            //判断输赢
            const size = this.chessStore.boardDetail.size
            let winner = this.checkWinner(this.chessStore.board, size)
            console.log(winner)
            if (winner !== null) {
                //代表此次操作有胜负,更新结果
                this.chessStore.changeGameStatus(GameStatus.WINNING)
                alert(winner)
            }
            this.chessStore.reversePlayer()
        },
        /**
         * 画整体棋盘
         * @param width 所需棋盘整体大小,上下左右预留一半space空间
         * @param size 线条数,线条数*间距=width
         * @param space 间距
         */
        drawBoard(width, size, space) {
            const halfSpace = space / 2;

            const canvas = document.getElementById("board");
            const ctx = canvas.getContext("2d");
            // 设置线条颜色
            ctx.strokeStyle = "black";

            for (let i = 0; i < size; i++) {
                // 绘制横线
                ctx.beginPath();
                ctx.moveTo(halfSpace, i * space + halfSpace);
                ctx.lineTo(width - halfSpace, i * space + halfSpace);
                ctx.stroke();
                // 绘制竖线
                ctx.beginPath();
                ctx.moveTo(i * space + halfSpace, halfSpace);
                ctx.lineTo(i * space + halfSpace, width - halfSpace);
                ctx.stroke();
            }
        }
        ,
        /**
         * 绘制棋子
         * @param x
         * @param y
         */
        drawChess(x, y) {
            const detail = this.chessStore.boardDetail
            const board = this.chessStore.board
            const current_player = this.chessStore.current_player
            //存在输赢以后,不允许在落子
            if (this.chessStore.status !== GameStatus.GAMING) {
                return;
            }

            const lineNumber = detail.size
            const space = detail.space
            const halfSpace = space / 2;


            // console.log(event.offsetX + '   ' + event.offsetY + '  ' + space)
            // 判断该位置是否有棋子
            // console.log(board)
            if (board[x][y] !== 0) {
                alert("该位置已有棋子")
                return;
            }

            const canvas = document.getElementById("board");
            const ctx = canvas.getContext("2d");
            //画带渐变色的棋子,同心圆形式
            //考虑起点为2,因半径为space一半,避免太大,截止1/3大小
            let grd = ctx.createRadialGradient(
                x * space + halfSpace,
                y * space + halfSpace,
                2,
                x * space + halfSpace,
                y * space + halfSpace,
                space / 3
            )
            grd.addColorStop(0,
                current_player === PlayerStatus.WHITE ? '#FFFFFF' : '#4C4C4C')
            grd.addColorStop(1,
                current_player === PlayerStatus.WHITE ? '#DADADA' : '#000000')
            ctx.beginPath()
            ctx.fillStyle = grd
            //画圆,半径设置为space/3,同上r1参数一致
            ctx.arc(
                x * space + halfSpace,
                y * space + halfSpace,
                space / 3,
                0,
                2 * Math.PI,
                false
            );
            ctx.fill();
            ctx.closePath();
            board[x][y] = current_player; //将黑白棋信息存储
            console.log(this.chessStore.board)
            this.chessStore.history.push({x, y, current_player})
        }
        ,
        /**
         * 胜负检查
         * @param board X*X 二维数组
         * @param lineNumber 线条数
         * @returns {UnwrapRef<string>|null}
         */
        checkWinner(board, lineNumber) {
            const current_player = this.chessStore.current_player
            // 检查横向是否有五子连线
            for (let i = 0; i < lineNumber; i++) {
                let count = 0;
                for (let j = 0; j < lineNumber; j++) {
                    if (board[i][j] === current_player) {
                        count++;
                    } else {
                        count = 0;
                    }

                    if (count >= 5) return current_player;
                }
            }

            // 检查纵向是否有五子连线
            for (let j = 0; j < lineNumber; j++) {
                let count = 0;
                for (let i = 0; i < lineNumber; i++) {
                    if (board[i][j] === current_player) {
                        count++;
                    } else {
                        count = 0;
                    }

                    if (count >= 5) return current_player;

                }
            }

            // 检查右斜线是否有五子连线
            for (let i = 0; i < lineNumber - 5; i++) {
                for (let j = 0; j < lineNumber - 5; j++) {
                    let count = 0;
                    for (let k = 0; k < 5; k++) {
                        if (board[i + k][j + k] === current_player) {
                            count++;
                        } else {
                            count = 0;
                        }

                        if (count >= 5) return current_player;

                    }
                }
            }

            // 检查左斜线是否有五子连线
            for (let i = 0; i < lineNumber - 5; i++) {
                for (let j = 4; j < lineNumber; j++) {
                    let count = 0;
                    for (let k = 0; k < 5; k++) {
                        if (board[i + k][j - k] === current_player) {
                            count++;
                        } else {
                            count = 0;
                        }

                        if (count >= 5) return current_player;
                    }
                }
            }

            // 如果没有五子连线,则游戏继续
            return null;
        },
        /**
         * 重置游戏
         */
        restartGame() {
            //清空基础数据
            this.initGame()
            //清空画布
            const boardDetail = this.chessStore.boardDetail
            const canvas = document.getElementById("board");
            const ctx = canvas.getContext("2d");
            ctx.clearRect(0, 0, canvas.width, canvas.height)
            //重新绘制,会初始化棋子信息
            this.drawBoard(boardDetail.width,
                boardDetail.size,
                boardDetail.space);
            //开始处理游戏状态
            this.chessStore.changeGameStatus(GameStatus.GAMING)
            //根据情况自动下棋
            console.log("==================重新开始=============")
            const res = start(boardDetail.size, this.chessStore.aiFirst, this.chessStore.depth)
            if (res !=null){
                //代表AI先行,存在旗子,则进行绘制
                this.drawChess(res[0], res[1])
                //交换选手
                this.chessStore.reversePlayer()
            }
            // 无需交换棋子,直接黑子开始
        },

    }
}
;
</script>

<template>
    <div class="main-board">
        <canvas id="board" class="board-chess" width="375" height="375"
                @click="handleDropChess($event)">
        </canvas>
    </div>
</template>

<style scoped>
.main-board {
    margin: 10px 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.board-chess {
    border: 3px solid black;
}
</style>

OperateBoard.vue 和上次类似

<script>
import {useChessStore} from "@/stores/chess.js";
import {GameStatus, PlayerStatus} from "@/stores/status.js";

export default {
    setup() {
        const chessStore = useChessStore()
        return {
            chessStore,
            PlayerStatus,
            GameStatus
        }
    },

    methods: {
        /**
         * 改变棋盘大小
         */
        changeBoardDetail() {
            const square = this.chessStore.boardDetail.width
            const newWidth = square
            const newHeight = square
            const newLineNumber = this.chessStore.boardDetail.size
            const newSpace = newWidth / newLineNumber
            // console.log(newWidth + '  ' + newLineNumber + '  ' + newSpace)
            //重新设置board大小
            let canvas = document.getElementById("board")
            canvas.width = newWidth
            canvas.height = newHeight
            //重新设置棋盘大小
            this.chessStore.changeBoardDetail(newWidth, newLineNumber, newSpace)
            //重新绘制游戏
            this.handleRestart()
        },
        /**
         * 重新开始
         */
        handleRestart() {
            this.chessStore.changeGameStatus(GameStatus.RESTART)
        }
    }
};
</script>

<template>
    <div class="operate-board">
        <div class="detail">
            <span>长宽:</span><input v-model="chessStore.boardDetail.width"/>
            <span>线条数:</span><input v-model="chessStore.boardDetail.size"/>
            <span>间距:</span><input v-model="chessStore.boardDetail.space" disabled/>
            <button @click="changeBoardDetail()">修改</button>
        </div>
        <div class="operate">
            <div>
                <span>AI先手</span>
                <input type=checkbox v-model="chessStore.aiFirst">
                <button @click="handleRestart">
                    <span v-if="chessStore.status===GameStatus.IDLE">开始</span>
                    <span v-else>重新开始</span>
                </button>
            </div>
            <div>
                <span>当前落子:{{ chessStore.current_player === PlayerStatus.WHITE ? "白" : "黑" }}</span>
                <span>胜利方:{{
                        chessStore.status === GameStatus.WINNING ?
                            (chessStore.current_player === PlayerStatus.WHITE ? "白棋" : "黑棋") : ""
                    }}</span>
            </div>
        </div>
    </div>
</template>

<style scoped>
.operate-board {
    margin: 0 10px;
    text-align: center;
}

.operate-board .detail {
    margin: 10px 0;
}

.operate-board .detail span {
    margin: 5px 0;
}

.operate-board .detail input {
    width: 40px;
    margin: 0 5px 0 0;
}

.operate-board .operate {
    display: flex;
    justify-content: center;
    text-align: center;
}

.operate-board .operate div {
    display: flex;
    text-align: center;
    align-items: center;
}
.operate-board .operate div button {
    margin: 0 10px;
}
.operate-board .operate span {
    margin: 0 5px;
}
</style>
最后修改:2024 年 12 月 17 日
如果觉得我的文章对你有用,请随意赞赏