构建画布并获取canvas 2d绘图引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const canvas = document.createElement("canvas");

// 设置canvas容器宽高,注意不推荐使用css设置宽高
canvas.width = 800;
canvas.height = 800;

// 设置背景色、位置等
canvas.style["display"] = "block"
canvas.style["margin"] = "0 auto"
canvas.style["background"] = "#0a0"

// 将canvas元素追加到body中
document.body.append(canvas);

// 获取画笔
const ctx = canvas.getContext("2d");

绘制棋盘

  • 棋盘由多条横线和纵线组合而成
  • 由于设定宽度为800,预留100空间,实际的棋盘宽为700
  • 为了好计算,棋盘切分为14格,每格宽为50
  • 循环14次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const canvas = document.createElement("canvas");
......
// 获取画笔
const ctx = canvas.getContext("2d");

for (let i = 1; i <= 15; i++) {
// 绘制横线
ctx.moveTo(50, 50 * i);
ctx.lineTo(750, 50 * i);
ctx.stroke();

// 绘制纵线
ctx.moveTo(50 * i, 50);
ctx.lineTo(50 * i, 750);
ctx.stroke();
}

鼠标点击生成棋子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

function drawPiece(x, y) {
// 绘制一个新的路径,一般与closePath一起使用
ctx.beginPath()
// 绘制一个半径为20的圆
ctx.arc(x, y, 20, 0, 2 * Math.PI)
ctx.fill()
ctx.closePath()
}

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e
let x = offsetX
let y = offsetY
// 绘制棋子
drawPiece(x, y)
})

现在点击鼠标,一个黑色的圆就显示在鼠标的位置上,我们预想的棋子是需要落在棋盘的框线上,这里就需要添加一些算法来确定棋子的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e

// 由于在定义棋盘时,预留100的长度,所以第1条线的位置为50,第2条线的位置在100,以此类推
// 可以得知,横纵线的坐标值都是整数,可以通过 Math.floor(鼠标的坐标 / 50) * 50 来得到整数坐标
// 计算棋子的位置
let x = Math.floor(offsetX / 50) * 50
let y = Math.floor(offsetY / 50) * 50
// 绘制棋子
drawPiece(x, y)
})

现在点击鼠标,一个棋子就出现在棋盘的框线位置上,我们在点格子的右下角时,预想的棋子是需要落在最近的框线上,但现在出现在左上角,这里还需要改良一下坐标算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e

// 由于在定义棋盘时,预留100的长度,所以第1条线的位置为50,第2条线的位置在100,以此类推
// 可以得知,横纵线的坐标值都是整数,可以通过 Math.floor(鼠标的坐标 / 50) * 50 来得到整数坐标
// 因为一个格子宽是50,我们可以将 鼠标的坐标值 都加上 25 去计算,这样就能落到最近的框线上
// 这里需要点想象空间,可以说是只能意会,言传很难
// 计算棋子的位置
let x = Math.floor((offsetX + 25) / 50) * 50
let y = Math.floor((offsetY + 25) / 50) * 50
// 绘制棋子
drawPiece(x, y)
})

现在点击鼠标,一个棋子就出现在棋盘的框线位置上,我们在点格子的右下角时,棋子是会落在最近的框线上,接下来我们美化一下棋子

绘制棋子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

function drawPiece(x, y) {
let tx = x - 10;
let ty = y - 10;
ctx.beginPath();
ctx.arc(x, y, 20, 0, 2 * Math.PI);
const g = ctx.createRadialGradient(tx, ty, 0, tx, ty, 30);
g.addColorStop(0, "#ccc");
g.addColorStop(1, "#000");
ctx.fillStyle = g;
ctx.fill();
ctx.closePath();
}

现在点击鼠标,一个有立体感的黑棋出现在棋盘的框线位置上,既然有黑棋,那么白棋也不能少,接下来我们添加一下棋子

绘制黑白棋

  • 五子棋玩法,黑棋下一子,然后到白棋下一子,再到黑棋下一子,依次循环
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
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

// 当前是否为黑棋
let isBlack = true

// 绘制棋子
function drawPiece(x, y) {
let tx = isBlack ? x - 10 : x + 10;
let ty = isBlack ? y - 10 : y + 10;
ctx.beginPath();
ctx.arc(x, y, 20, 0, 2 * Math.PI);
const g = ctx.createRadialGradient(tx, ty, 0, tx, ty, 30);
g.addColorStop(0, isBlack ? "#ccc" : "#666");
g.addColorStop(1, isBlack ? "#000" : "#fff");
ctx.fillStyle = g;
ctx.fill();
ctx.closePath();
}

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e

// 由于在定义棋盘时,预留100的长度,所以第1条线的位置为50,第2条线的位置在100,以此类推
// 可以得知,横纵线的坐标值都是整数,可以通过 Math.floor(鼠标的坐标 / 50) * 50 来得到整数坐标
// 因为一个格子宽是50,我们可以将 鼠标的坐标值 都加上 25 去计算,这样就能落到最近的框线上
// 这里需要点想象空间,可以说是只能意会,言传很难
// 计算棋子的位置
let x = Math.floor((offsetX + 25) / 50) * 50
let y = Math.floor((offsetY + 25) / 50) * 50
// 绘制棋子
drawPiece(x, y)

// 切换黑白棋
isBlack = !isBlack
})

点击鼠标在棋盘上显示不同的黑棋、白棋,会发现有一点bug,黑白棋会互相覆盖,接下来解决这个问题

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
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

// 当前是否为黑棋
let isBlack = true
// 使用一个二维数组,索引作为棋子的坐标
let pieces = []
// 初始化二维数组
for (let i = 1; i <= 15; i++) {
pieces[i] = []
}

......

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e

// 计算棋子的位置
let i = Math.floor((offsetX + 25) / 50)
let j = Math.floor((offsetY + 25) / 50)

// 判断当前位置是否已经存在棋子
if (pieces[j][i]) {
return;
}

// 将当前索引在二维数组中标记为黑棋或白棋
pieces[j][i] = isBlack ? "black" : "white";

// 由于在定义棋盘时,预留100的长度,所以第1条线的位置为50,第2条线的位置在100,以此类推
// 可以得知,横纵线的坐标值都是整数,可以通过 Math.floor(鼠标的坐标 / 50) * 50 来得到整数坐标
// 因为一个格子宽是50,我们可以将 鼠标的坐标值 都加上 25 去计算,这样就能落到最近的框线上
// 这里需要点想象空间,可以说是只能意会,言传很难
// 计算棋子的位置
let x = i * 50
let y = j * 50
// 绘制棋子
drawPiece(x, y)

// 切换黑白棋
isBlack = !isBlack
})

这时黑白棋的交替出现基本上没有问题了,这里还有一些bug,就是点击棋盘边缘时,棋子出现在棋盘外,接下来解决这个bug

1
2
3
4
5
6
7
8
9
10
11
12
13
......
// 获取画笔
const ctx = canvas.getContext("2d");
......

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e
// 如果鼠标位置不在棋盘上,不做任何操作
if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) return;

......
})

至此,黑白棋交替出现已经完成了,接下来添加一些提示文本

添加提示文本

  • 添加在canvas元素之前,为了方便,使用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
const tip = document.createElement("div");
tip.style["text-align"] = "center"
tip.style["padding"] = "0 0 5px"
tip.innerText = '请黑棋落子'
document.body.append(tip);

......
// 获取画笔
const ctx = canvas.getContext("2d");
......

canvas.addEventListener("click", (e) => {
......

// 判断当前位置是否已经存在棋子
if (pieces[j][i]) {
tip.innerText = `这里不能重复落子,当前是${isBlack ? "黑" : "白"}子的回合`;
return;
}

......

// 切换黑白棋
isBlack = !isBlack
tip.innerText = isBlack ? "请黑棋落子" : "请白棋落子";
})

实现五子棋游戏规则

  • 连续5个相同颜色的棋子方获胜
  • 加上本体,向上4个、向下4个、向左4个、向右4个、向左上4个、向右上4个、向左下4个、向右下4个,连续相同的颜色获胜
  • 从上到下有连续5个相同颜色的棋子就能获胜
  • 从左到右有连续5个相同颜色的棋子就能获胜
  • 从左上到右下连续5个相同颜色的棋子就能获胜
  • 从右上到左下连续5个相同颜色的棋子就能获胜
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
......
// 获取画笔
const ctx = canvas.getContext("2d");
......
// 二维数组记录棋子
let pieces = [];
// 初始化
for (let i = 1; i <= 15; i++) {
pieces[i] = [];
}
// 定义一个变量是否结束游戏
let endGame = false;

canvas.addEventListener("click", (e) => {
// 当前鼠标位于canvas相对位置
const { offsetX, offsetY } = e;
// 如果鼠标位置不在棋盘上,不做任何操作
if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) return;

if (endGame) {
// 已经结束游戏了
return;
}

......

// 在鼠标位置上绘制黑棋
drawPiece(x, y);

endGame =
checkVertical(j, i) ||
checkHorizontal(j, i) ||
checkNW2SE(j, i) ||
checkNE2SW(j, i);
if (endGame) {
// 证明已经有人获胜
tip.innerText = `${isBlack ? "黑棋" : "白棋"}获胜,请刷新重新开始`;
return;
}

// 切换黑白棋
isBlack = !isBlack;
tip.innerText = isBlack ? "请黑棋落子" : "请白棋落子";
})

// 纵向查找是否有连续5个相同的棋子
function checkVertical(row, col) {
// 定义一个向上的索引器
let up = 0;
// 定义一个向下的索引器
let down = 0;
// 定义一个计数器,用来记录连续几个相同的
let count = 1;
// 只需要循环4次,当前棋子不需要计算,根据五子棋规则(算法可再优化)
for (let times = 0; times < 4; times++) {
let target = isBlack ? "black" : "white";
// 向上查找
up++;
// 当行数小于向上索引,则不做任何处理
if (row - up >= 1) {
if (pieces[row - up][col] && pieces[row - up][col] === target) {
count++;
}
}
// 向下查找
down++;
// 当行数大于向下索引,则不做任何处理
if (row + down <= 15) {
if (pieces[row + down][col] && pieces[row + down][col] === target) {
count++;
}
}

// 累计5次就匹配成功
if (count >= 5) {
break;
}
}
return count >= 5;
}

// 横向查找是否有连续5个相同的棋子
function checkHorizontal(row, col) {
// 定义一个向左的索引器
let left = 0;
// 定义一个向右的索引器
let right = 0;
// 定义一个计数器,用来记录连续几个相同的
let count = 1;
// 只需要循环4次,当前棋子不需要计算,根据五子棋规则(算法可再优化)
for (let times = 0; times < 4; times++) {
let target = isBlack ? "black" : "white";
// 向左查找
left++;
// 当列数小于向左索引,则不做任何处理
if (col - left >= 1) {
if (pieces[row][col - left] && pieces[row][col - left] === target) {
count++;
}
}
// 向右查找
right++;
// 当列数大于向右索引,则不做任何处理
if (col + right <= 15) {
if (pieces[row][col + right] && pieces[row][col + right] === target) {
count++;
}
}

// 累计5次就匹配成功
if (count >= 5) {
break;
}
}
return count >= 5;
}

// 从左上到右下查找是否有连续5个相同的棋子
function checkNW2SE(row, col) {
// 定义一个向左上的索引器
let lt = 0;
// 定义一个向右下的索引器
let rb = 0;
// 定义一个计数器,用来记录连续几个相同的
let count = 1;
// 只需要循环4次,当前棋子不需要计算,根据五子棋规则(算法可再优化)
for (let times = 0; times < 4; times++) {
let target = isBlack ? "black" : "white";
// 向左上查找
lt++;
// 当行列数都小于向左上索引,则不做任何处理
if (row - lt >= 1 && col - lt >= 1) {
if (pieces[row - lt][col - lt] && pieces[row - lt][col - lt] === target) {
count++;
}
}
// 向右下查找
rb++;
// 当行列数大于向右下索引,则不做任何处理
if (row + rb <= 15 && col + rb <= 15) {
if (pieces[row + rb][col + rb] && pieces[row + rb][col + rb] === target) {
count++;
}
}

// 累计5次就匹配成功
if (count >= 5) {
break;
}
}
return count >= 5;
}

// 从左上到右下查找是否有连续5个相同的棋子
function checkNE2SW(row, col) {
// 定义一个向右上的索引器
let rt = 0;
// 定义一个向左下的索引器
let lb = 0;
// 定义一个计数器,用来记录连续几个相同的
let count = 1;
// 只需要循环4次,当前棋子不需要计算,根据五子棋规则(算法可再优化)
for (let times = 0; times < 4; times++) {
let target = isBlack ? "black" : "white";
// 向右上查找
rt++;
// 当行数小于向右上索引、列数大于向右上索引,则不做任何处理
if (row - rt >= 1 && col + rt <= 15) {
if (pieces[row - rt][col + rt] && pieces[row - rt][col + rt] === target) {
count++;
}
}
// 向左下查找
lb++;
// 当行数大于向左下索引、列数小于向左下索引,则不做任何处理
if (row + lb <= 15 && col - lb >= 1) {
if (pieces[row + lb][col - lb] && pieces[row + lb][col - lb] === target) {
count++;
}
}

// 累计5次就匹配成功
if (count >= 5) {
break;
}
}
return count >= 5;
}

上述的算法,可能还有优化的地步,暂时未想到更好的。至此一个五子棋的极简版已完成,后续会完善

源码学习

五子棋 - CodePenhttps://codepen.io/EvilChan/pen/jOeYmZr