在上次框架改版、引入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>