慕课网-人机大战五子棋AI篇学习笔记

  • 创建赢法数组

  1. 先贴出第一步代码

    1
    2
    3
    4
    5
    6
    7
    var wins = [];
    for(let i=0 ; i < 15 ; i++ ) {
    wins[i] = [];
    for(let j = 0; j<15; j++) {
    wins[i][j] = [];
    }
    }

    这段代码仅仅只是创了一个准备用于记录所有赢法的三维空数组

  1. 接下来第二部就是建立赢法数组,复制并建立索引。
    代码:竖排
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var count = 0;
    for(let i=0; i< 15 ;i++) {
    for(let j=0;j<11 ;j++) {
    for(let k=0;k<5;k++) {
    wins[i][j+k][count] = true;
    }
    count++;
    }
    }

本段代码的含义就是遍历了所有竖排列中存在的赢的组合,count为每一种赢法所对应的唯一索引。
例如:

1
2
3
4
5
wins[0][0][0] = true;
wins[0][1][0] = true;
wins[0][2][0] = true;
wins[0][3][0] = true;
wins[0][4][0] = true;

就是意味着我们将第一竖排前五个空格连成一线的赢法都列举出来,并给每一个格子赋值为true、给出的索引为count = 0(即第三维数组)。

同理,很容易可以写出遍历横排,45°角和135°角排列上的代码:

横排

1
2
3
4
5
6
7
8
9
10
var count = 0;
for(let i=0; i< 15 ;i++) {
for(let j=0;j<11 ;j++) {
for(let k=0;k<5;k++) {
wins[j+k][i][count] = true;
}
count++;
}
}

45°

1
2
3
4
5
6
7
8
9
10
var count = 0;
for(let i=0; i< 11 ;i++) {
for(let j=0;j<11 ;j++) {
for(let k=0;k<5;k++) {
wins[i+k][j+k][count] = true;
}
count++;
}
}

135°

1
2
3
4
5
6
7
8
9
10
var count = 0;
for(let i=0; i< 11 ;i++) {
for(let j=14;j>3 ;j++) {
for(let k=0;k<5;k++) {
wins[i+k][j-k][count] = true;
}
count++;
}
}


相较于较为抽象的描述,还是图来的简单明了。
image

输赢判断

慕课网将其称为赢法统计,我总觉得过于抽象了。让我来理解,说是输赢判断,或者说是迈向胜利的进度,更加容易理解。
代码:

1
2
3
4
5
6
7
var myWin = [];//我方胜利进度(黑子)
var coumputerWin = [];//电脑方胜利进度(白子)
for(let i = 0;i < count ; i++) {
myWin[i] = 0;
coumputerWin[i] = 0;
}

首相遍历count,两方胜利进度数组都赋值为0。

然后就要修改落子时的onclick事件的判定条件。

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
var over = false;//判断棋局是否结束
chess.onclick = function(e) {
if(over) {
return;
}
var x = e.offsetX;
var y = e.offsetY;
var i= Math.floor(x/30);
var j = Math.floor(y/30);
if(hascheer[i][j] == 0) {
oneStep(i,j,me);
if(me) {
hascheer[i][j] = 1;
}else {
hascheer[i][j] = 2;
}
me = !me;
//本次操作添加代码
for(let k= 0;k<count; k++) {
if(wins[i][j][k]) {
myWin[k] ++;
pcWin = 6;//当有一方占据了当前赢法数组中的一格,即可判定为另一方在该赢法数组条件下已不可能获胜,抛出异常数值。
if(myWin[k] == 5) {
window.alert("你赢了!");
}
}
}
}
}

前面的代码是UI篇当中的。关于输赢判断,思路是在onclick事件下,每次落子,遍历count,并对落子坐标i,j所对应的赢法k胜利进度+1,胜利进度=5的时候,就代表在这一赢法下获胜了。

例如,在wins[0][0][k]的情况下,其实只有三种赢法,即向右五格,向下五格以及45°角五格,因此,当k不等于这三种赢法的索引时,myWins[k]就不会自加,即该赢法进度没有变化。PS:后面要做computer自动下子,所以这里只写了黑子的判定


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
var pcAI = function() {
//玩家分数
let myScore = [],
// 电脑分数
pcScore = [],
// 最优位置的分数
max = 0,
// 电脑预落子maxI列
maxI = 0,
// 电脑预落子maxJ行
maxJ =0;
for (let i=0;i<15;i++) {
myScore[i] = [];
pcScore[i] = [];
for(let j=0; j<15;j++) {
myScore[i][j] = 0;
pcScore[i][j] = 0;
}
}
// 对两个数组分别进行加分
for (let i=0;i<15;i++) {
for(let j=0;j<15;j++) {
if(hasChess[i][j] == 0) {
for (let k=0;k<count;k++){
if(wins[i][j][k]) {
switch(myWin[k]) {
case 1:
myScore[i][j] +=200;
break;
case 2:
myScore[i][j] +=400;
break;
case 3:
myScore[i][j] +=2000;
break;
case 4:
myScore[i][j] +=10000;
break;
default:
break;
}
switch(pcWin[k]) {
case 1:
pcScore[i][j] +=220;
break;
case 2:
pcScore[i][j] +=420;
break;
case 3:
pcScore[i][j] +=2220;
break;
case 4:
pcScore[i][j] +=20000;
break;
case 6:
pcScore[i][j] = 0;
break;
default:
break;
}
}
}
// 若玩家在(i,j)处的分数高于目前的最高分数,则落子在(i,j)处
if(myScore[i][j] > max) {
max = myScore[i][j];
maxI = i;
maxJ = j;
}else if(myScore[i][j] == max&& pcScore[i][j] > pcScore[maxI][maxJ]){
// 如果玩家(i,j)处和目前最优分数一样大,则比较电脑在该位置和预落子的位置的分数
maxI = i;
maxJ = j;
}
// 如果电脑(i,j)处比目前最优的分数大,则落子在(i,j)处
if(pcScore[i][j] > max) {
max = pcScore[i][j];
maxI = i;
maxJ = j;
}else if(pcScore[i][j] == max && myScore[i][j] > myScore[maxI][maxJ]){
// 如果电脑(i,j)处和目前最优分数一样大,则比较玩家在该位置和预落子的位置的分数
maxI = i;
maxJ = j;
}
}
};
}
oneStep(maxI,maxJ,false);
hasChess[maxI][maxJ] = 2;
for (let n=0 ; n<count;n++) {
if(wins[maxI][maxJ][n]) {
pcWin[n]++;
myWin[n] =6;
if(pcWin[n] == 5) {
window.alert("你输了!");
over = true;
}
}
}
}

很复杂的代码。通过遍历坐标,确定包含这个坐标的赢法,然后对这个赢法进行权重加成,赢法内拥有越多的黑子,权重越大。同时,对自己下子的每个坐标所对应的的赢法也进行加权,并且电脑获胜是的权重是高于人的。(这里很难用自己的话描述出来)。
以图为例:

image

当黑子落点为坐标[0][0]时,此时应当是有三个方向上,即横、竖、45°,三个赢法数组存在。并且所对应的元素均为true。黑子落点后,上面代码中的第一次循环因为if(hasCheer[0][0])为false,跳过,三种赢法内空余的格子都进入循环,每种myWin[k]都是一,所以横、竖、45°线上其他空格的分数均为200,因此第一次循环获取到坐标i=0,j=之后,后面的因为都与它相等不会获得更高权重,所以AI会落子在0,1处。


完整代码

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
//canvas
var gobang = document.getElementById("gobang"),
context = gobang.getContext("2d"),
background = new Image();
background.src = "img/bg.png";
context.strokeStyle = "#000";
background.onload = function() {
context.drawImage(background,0,0,450,450);
(() => {
for (let i= 0; i<15 ; i++ ) {
context.moveTo(15 + i*30, 15);
context.lineTo(15 + i*30, 435);
context.stroke();
context.moveTo(15,15 + i*30);
context.lineTo(435,15+ i*30);
context.stroke();
}
})();
}
//落子
var hasChess = [],
me = true;
for (let i=0; i<15; i++) {
hasChess[i] = [];
for (let j=0; j<15;j++) {
hasChess[i][j] = 0;
}
}
var oneStep = function (i,j,me) {
context.beginPath();
context.arc(15 + i*30,15 + j*30,13,0,2*Math.PI);
context.closePath();
let gradient = context.createRadialGradient(15 + i*30, 15+ j*30 -2,50,15 + i*30 +2, 15+j * 30 - 2,0);
me ? (()=>{gradient.addColorStop(0,"#0a0a0a");gradient.addColorStop(1,"#8e8e8e" )})() : (()=>{gradient.addColorStop(0,"#8e8e8e");gradient.addColorStop(1,"#fff")})();
context.fillStyle = gradient;
context.fill();
me = !me;
}
//赢法数组
var wins = [],
count = 0;
//生产赢法数组
for (let i=0;i<15 ; i ++) {
wins[i] = [];
for(let j = 0; j<15;j ++) {
wins[i][j] = [];
}
}
//所有竖排赢法
for (let i=0; i<15; i++) {
for(let j=0;j<11;j++) {
for (let k=0;k<5;k++) {
wins[i][j+k][count] = true;
}
count++;
}
}
//所有横排赢法
for (let i=0; i<15;i++) {
for (let j = 0;j<11;j ++) {
for (let k=0;k<5;k++) {
wins[j+k][i][count] = true;
}
count++;
}
}
//所有45°赢法
for (let i=0;i<11;i++) {
for(let j=0;j<11;j++) {
for(let k=0;k<5;k++) {
wins[i+k][j+k][count] = true;
}
count++;
}
}
//所有135°赢法
for (let i=0; i<11;i++) {
for(let j=14;j>3;j--) {
for (let k=0;k<5;k++) {
wins[i+k][j-k][count] = true;
}
count++;
}
}
//输赢判断
var myWin = [],
pcWin = [],
over = false;
for(let i=0;i<count ;i++) {
myWin[i] = 0;
pcWin[i] = 0;
}
//AI实现
var pcAI = function() {
//玩家分数
let myScore = [],
// 电脑分数
pcScore = [],
// 最优位置的分数
max = 0,
// 电脑预落子maxI列
maxI = 0,
// 电脑预落子maxJ行
maxJ =0;
for (let i=0;i<15;i++) {
myScore[i] = [];
pcScore[i] = [];
for(let j=0; j<15;j++) {
myScore[i][j] = 0;
pcScore[i][j] = 0;
}
}
// 对两个数组分别进行加分
for (let i=0;i<15;i++) {
for(let j=0;j<15;j++) {
if(hasChess[i][j] == 0) {
for (let k=0;k<count;k++){
if(wins[i][j][k]) {
switch(myWin[k]) {
case 1:
myScore[i][j] +=200;
break;
case 2:
myScore[i][j] +=400;
break;
case 3:
myScore[i][j] +=2000;
break;
case 4:
myScore[i][j] +=10000;
break;
default:
break;
}
switch(pcWin[k]) {
case 1:
pcScore[i][j] +=220;
break;
case 2:
pcScore[i][j] +=420;
break;
case 3:
pcScore[i][j] +=2220;
break;
case 4:
pcScore[i][j] +=20000;
break;
case 6:
pcScore[i][j] = 0;
break;
default:
break;
}
}
}
// 若玩家在(i,j)处的分数高于目前的最高分数,则落子在(i,j)处
if(myScore[i][j] > max) {
max = myScore[i][j];
maxI = i;
maxJ = j;
}else if(myScore[i][j] == max&& pcScore[i][j] > pcScore[maxI][maxJ]){
// 如果玩家(i,j)处和目前最优分数一样大,则比较电脑在该位置和预落子的位置的分数
maxI = i;
maxJ = j;
}
// 如果电脑(i,j)处比目前最优的分数大,则落子在(i,j)处
if(pcScore[i][j] > max) {
max = pcScore[i][j];
maxI = i;
maxJ = j;
}else if(pcScore[i][j] == max && myScore[i][j] > myScore[maxI][maxJ]){
// 如果电脑(i,j)处和目前最优分数一样大,则比较玩家在该位置和预落子的位置的分数
maxI = i;
maxJ = j;
}
}
};
}
oneStep(maxI,maxJ,false);
hasChess[maxI][maxJ] = 2;
for (let n=0 ; n<count;n++) {
if(wins[maxI][maxJ][n]) {
pcWin[n]++;
myWin[n] =6;
if(pcWin[n] == 5) {
window.alert("你输了!");
over = true;
}
}
}
}
gobang.onclick = function(e) {
if (over) {
return;
}
if (!me) {
return;
}
let x = e.offsetX,
y = e.offsetY,
i = Math.floor(x/30),
j = Math.floor(y/30);
if (hasChess[i][j] == 0 ) {
oneStep(i,j,me);
hasChess[i][j] = 1;
for (let k=0; k<count;k++) {
if(wins[i][j][k]) {
myWin[k]++;
pcWin[k] =6;
if(myWin[k] == 5) {
window.alert("你赢了!");
over = true;
}
}
}
if(!over) {
pcAI();
}
}
}