VUE3+Canvas绘制五子棋(五)

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

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

本次主要重点引入的代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//默认宽度
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 状态管理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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 此部分代码和上次无太大差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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此部分整体框架和上次类似,功能描述上有调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
<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 和上次类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<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>

VUE3+Canvas绘制五子棋(五)
http://060800.xyz/2024/12/17/VUE3-Canvas绘制五子棋(五)/
作者
砖头
发布于
2024年12月17日
许可协议