1
|
|
|
/** |
2
|
|
|
* A module to calculate a move in Gomoku |
3
|
|
|
* |
4
|
|
|
* Ripped and modified from Zebra_Gomoku by Stefan Gabos. |
5
|
|
|
* |
6
|
|
|
* Some day I have to write my own... |
7
|
|
|
* |
8
|
|
|
* Visit {@link https://github.com/stefangabos/Zebra_Gomoku} for more information. |
9
|
|
|
*/ |
10
|
|
|
"use strict"; |
11
|
|
|
|
12
|
|
|
function nextBestMove(board, boardSize) { |
13
|
|
|
var m, |
14
|
|
|
l, |
15
|
|
|
position, |
16
|
|
|
type, |
17
|
|
|
line, |
18
|
|
|
totalCells, |
19
|
|
|
consecutiveCells, |
20
|
|
|
emptySides, |
21
|
|
|
bestScore, |
22
|
|
|
cellScore, |
23
|
|
|
directionScore, |
24
|
|
|
score; |
25
|
|
|
|
26
|
|
|
// iterate through all the board's cells |
27
|
16 |
|
for (var i = boardSize * boardSize; i--; ) { |
28
|
|
|
// skip to next cell if this cell is owned by the computer |
29
|
|
|
// if (board[i] == 1) continue; |
30
|
|
|
|
31
|
|
|
// by default, the best move is the first free cell |
32
|
|
|
// (position, attack, defense) |
33
|
|
|
|
34
|
1600 |
|
if (bestScore === undefined) { |
|
|
|
|
35
|
16 |
|
bestScore = [i, 0, 0]; |
36
|
|
|
} |
37
|
|
|
// we will give a "score" to the position, |
38
|
|
|
// based on its surroundings horizontally, vertically and |
39
|
|
|
// on both diagonals; for example: for a row like 000XXPXX000 (where "0" means empty, |
40
|
|
|
// "X" represents the opponent's pieces and "P" is the position for which we are |
41
|
|
|
// determining the "score", we would check |
42
|
|
|
// 0|00XXP|XX000, 00|0XXPX|X000, 000|XXPXX|000, 000X|XPXX0|00, 000XX|PXX00|0, |
43
|
|
|
// and then we would do the same on the vertical, and on both diagonals) |
44
|
|
|
|
45
|
|
|
// cell's default score (attack and defense) |
46
|
1600 |
|
cellScore = [0, 0]; |
47
|
|
|
|
48
|
|
|
// the 4 directions to check: vertical, horizontal, diagonal /, diagonal \ (in this order) |
49
|
1600 |
|
for (var j = 4; j--; ) { |
50
|
|
|
// the default score for the direction we're checking |
51
|
6400 |
|
directionScore = [0, 0]; |
52
|
|
|
|
53
|
|
|
// check the 5 possible outcomes, as described above |
54
|
|
|
// (if we're checking whether the player won, |
55
|
|
|
// we'll do this iteration only once, checking for 5 in a row) |
56
|
6400 |
|
for (var k = !board[i] ? 5 : 1; k--; ) { |
57
|
|
|
// initialize the type of cells we're looking for, |
58
|
|
|
// and the array with the cells on the current direction |
59
|
15680 |
|
type = board[i] || undefined; |
60
|
15680 |
|
line = []; |
61
|
|
|
|
62
|
|
|
// check the 5 pieces for each possible outcome, plus the 2 sides |
63
|
15680 |
|
for (l = 7; l--; ) { |
64
|
|
|
// used to compute position |
65
|
73128 |
|
m = -5 + k + l; |
66
|
73128 |
|
var n = i % boardSize; |
67
|
|
|
|
68
|
73128 |
|
if ( |
69
|
|
|
// vertical |
70
|
|
|
((j === 0 && |
71
|
|
|
(position = i + boardSize * m) !== false && |
72
|
|
|
n == position % boardSize) || |
73
|
|
|
// horizontal |
74
|
|
|
(j == 1 && |
75
|
|
|
(position = i + m) !== false && |
76
|
|
|
~~(position / boardSize) == ~~(i / boardSize)) || |
77
|
|
|
// diagonal / |
78
|
|
|
(j == 2 && |
79
|
|
|
(position = i - boardSize * m + m) !== false && |
80
|
|
|
((position > i && position % boardSize < n) || |
81
|
|
|
(position < i && position % boardSize > n) || |
82
|
|
|
position == i)) || |
83
|
|
|
// diagonal \ |
84
|
|
|
((j == 3 && |
85
|
|
|
(position = i + boardSize * m + m) !== false && |
86
|
|
|
((position < i && position % boardSize < n) || |
87
|
|
|
(position > i && position % boardSize > n))) || |
88
|
|
|
position == i)) && |
|
|
|
|
89
|
|
|
// the position is not off-board |
90
|
|
|
position >= 0 && |
91
|
|
|
position < boardSize * boardSize && |
92
|
|
|
// the cell is of the same type as the ones we are looking for |
93
|
|
|
// or, we are checking the score for an empty cell, |
94
|
|
|
// the current position is empty, |
95
|
|
|
// or is "undefined" (meaning we didn't yet find any non-empty cells) |
96
|
|
|
(board[position] == type || |
97
|
|
|
(!board[i] && (!board[position] || undefined === type)) || |
98
|
|
|
// or we're just checking the sides |
99
|
|
|
!l || |
100
|
|
|
l == 6) |
101
|
|
|
) { |
102
|
|
|
// add position to the line |
103
|
56572 |
|
line.push(position); |
104
|
|
|
|
105
|
|
|
// if we're not just checking the sides, |
106
|
|
|
// this is not an empty cell, and is of the same type as |
107
|
|
|
// the ones we're looking for, |
108
|
|
|
// update the type of cells we're looking for |
109
|
|
|
// (we use ^ instead of !=) |
110
|
56572 |
|
if (l && l ^ 6 && undefined === type && board[position]) { |
111
|
1118 |
|
type = board[position]; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
// if the computed position is off-board, |
115
|
|
|
// but this is a side - cell, save it as "undefined" |
116
|
16556 |
|
} else if (!l || l == 6) { |
117
|
6532 |
|
line.push(undefined); |
118
|
|
|
} else { |
119
|
|
|
// skip the rest of the test if we found this row to be "non-compliant" |
120
|
|
|
// (a different type of cell than the ones we're looking for, |
121
|
|
|
// one of the 5 cells is off - board) |
122
|
10024 |
|
break; |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
// if we added exactly 7 position to our line, and |
127
|
|
|
// the line is not containing * only * empty cells |
128
|
15680 |
|
if (line.length == 7 && undefined !== type) { |
129
|
|
|
// if we are checking whether the player won, |
130
|
|
|
// set this flag so that later on we do not |
131
|
|
|
// overwrite the value of the cell |
132
|
1326 |
|
m = board[i] ? true : false; |
133
|
|
|
|
134
|
|
|
// calculate the score when setting the current cell to |
135
|
|
|
// the same type as the other ones we found |
136
|
1326 |
|
board[i] = type; |
137
|
|
|
|
138
|
|
|
// calculate the number of consecutive cells we get like this |
139
|
|
|
// (we'll do this by looking in our "line" array) |
140
|
1326 |
|
consecutiveCells = 0; |
141
|
1326 |
|
totalCells = 0; |
142
|
1326 |
|
emptySides = 0; |
143
|
|
|
|
144
|
|
|
// the total number of cells of the same type |
145
|
1326 |
|
for (l = 5; l--; ) { |
146
|
6630 |
|
if (board[line[l + 1]] == type) { |
147
|
4570 |
|
totalCells++; |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
// look to the left of the current cell |
152
|
1326 |
|
for ( |
153
|
|
|
l = line.indexOf(i) - 1; |
154
|
|
|
l >= 0; |
155
|
|
|
// if the cell is of the same type, |
156
|
|
|
//increment the number of consecutive cells |
157
|
|
|
l-- |
158
|
|
|
) { |
159
|
1528 |
|
if (board[line[l]] == type) { |
160
|
718 |
|
consecutiveCells++; |
161
|
|
|
} else { |
162
|
|
|
// otherwise |
163
|
|
|
// if the adjacent cell is empty, increment the number of empty sides |
164
|
|
|
// we have to use === 0 (instead of !) because it |
165
|
|
|
// can also be "undefined" |
166
|
810 |
|
if (board[line[l]] === 0) { |
167
|
646 |
|
emptySides++; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
// don't look further |
171
|
810 |
|
break; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
// look to the right of the current cell |
176
|
1326 |
|
for ( |
177
|
|
|
l = line.indexOf(i); |
178
|
|
|
l < line.length; |
179
|
|
|
// if the cell is of the same type, |
180
|
|
|
// increment the number of consecutive cells |
181
|
|
|
l++ |
182
|
|
|
) { |
183
|
5194 |
|
if (board[line[l]] == type) { |
184
|
4370 |
|
consecutiveCells++; |
185
|
|
|
} else { |
186
|
|
|
// otherwise |
187
|
|
|
// if the adjacent cell is empty, increment the number of empty sides |
188
|
|
|
// we have to use === 0 (instead of !) |
189
|
|
|
// because it can also be "undefined" |
190
|
824 |
|
if (board[line[l]] === 0) { |
191
|
652 |
|
emptySides++; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
// don't look further |
195
|
824 |
|
break; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
// give a score to the row based on the |
200
|
|
|
// array below(number of cells / empty sides) |
201
|
1326 |
|
score = [[0, 1], [2, 3], [4, 12], [10, 64], [256, 256]][ |
202
|
|
|
consecutiveCells >= totalCells |
203
|
|
|
? Math.min(consecutiveCells, 5) - 1 |
204
|
|
|
: totalCells - 1 |
205
|
|
|
][consecutiveCells >= totalCells ? (emptySides ? emptySides - 1 : 0) : 0]; |
206
|
|
|
|
207
|
|
|
// reset the cell's value (unless we are looking to see if the player won) |
208
|
1326 |
|
if (!m) { |
209
|
734 |
|
board[i] = 0; |
210
|
|
|
} |
211
|
|
|
// } else if (score >= 256) { |
212
|
|
|
// // if the player won, update the score |
213
|
|
|
// score = 1024; |
214
|
|
|
// } |
215
|
|
|
|
216
|
|
|
// if, so far, this is the best |
217
|
|
|
// attack / defense score(depending on the value of "type") |
218
|
|
|
// for this direction, update it |
219
|
1326 |
|
if (score > directionScore[type - 1]) { |
220
|
1016 |
|
directionScore[type - 1] = score; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// update the cell's attack and defense score |
226
|
|
|
// (we simply sum the best scores of all 4 directions) |
227
|
6400 |
|
for (k = 2; k--; ) { |
228
|
12800 |
|
cellScore[k] += directionScore[k]; |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
// used below |
233
|
1600 |
|
j = cellScore[0] + cellScore[1]; |
234
|
1600 |
|
k = bestScore[1] + bestScore[2]; |
235
|
|
|
|
236
|
|
|
// if cell's attack + defense score is better than |
237
|
|
|
// the current best attack and defense score |
238
|
|
|
// or, cell's score is equal to the best score, |
239
|
|
|
// but computer's move is better or equal to the player's, |
240
|
|
|
// and the current best move is not *exactly* the same |
241
|
1600 |
|
if ( |
242
|
|
|
(j > k || (j == k && cellScore[0] >= bestScore[1])) && |
243
|
|
|
// we're checking the score of an empty cell, |
244
|
|
|
// or we're checking to see if the player won and he won |
245
|
|
|
// (we don't update the score when checking if the |
246
|
|
|
// player won * unless * the player actually won) |
247
|
|
|
(!board[i] || cellScore[1] >= 1024) |
248
|
|
|
|
249
|
|
|
// update best score (position, attack, defense) |
250
|
|
|
) { |
251
|
234 |
|
bestScore = [i, cellScore[0], cellScore[1]]; |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
// Calculation done return x, y |
256
|
|
|
var x, y; |
257
|
|
|
|
258
|
16 |
|
y = Math.floor(bestScore[0] / boardSize); |
259
|
16 |
|
x = bestScore[0] % boardSize; |
260
|
|
|
|
261
|
16 |
|
return { x: x, y: y }; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
function getRandomIntInclusive(min, max) { |
265
|
4 |
|
return Math.floor(Math.random() * (max - min + 1)) + min; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
// randomMove :: ([Mark], Size) -> Position |
269
|
|
|
// Mark = 0 | 1 | 2 |
270
|
|
|
// Size = Integer |
271
|
|
|
// Position = { x: Integer, y: Integer } |
272
|
|
|
function randomMove(board, boardSize) { |
273
|
6 |
|
if ( |
274
|
|
|
board.every(function(x) { |
275
|
402 |
|
return x !== 0; |
276
|
|
|
}) |
277
|
|
|
) { |
278
|
4 |
|
return null; |
279
|
|
|
} |
280
|
|
|
var x, y; |
281
|
|
|
|
282
|
2 |
|
do { |
283
|
2 |
|
x = getRandomIntInclusive(0, boardSize); |
284
|
2 |
|
y = getRandomIntInclusive(0, boardSize); |
285
|
|
|
} while (board[x + y * boardSize] !== 0); |
286
|
|
|
|
287
|
2 |
|
return { x: x, y: y }; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
// bestMove :: ([Mark], Size) -> Position |
291
|
|
|
// Mark = 0 | 1 | 2 |
292
|
|
|
// Size = Integer |
293
|
|
|
// Position = { x: Integer, y: Integer } |
294
|
|
|
function bestMove(board, boardSize) { |
295
|
16 |
|
var pos = nextBestMove(board, boardSize); |
296
|
|
|
|
297
|
|
|
// On occasion the algoritm calcs a position that is already taken |
298
|
|
|
// Catch that and randomize something. |
299
|
16 |
|
if (board[pos.x + pos.y * boardSize] !== 0) { |
300
|
2 |
|
pos = randomMove(board, boardSize); |
301
|
|
|
} |
302
|
|
|
|
303
|
16 |
|
return pos; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
module.exports = { bestMove: bestMove, randomMove: randomMove }; |
307
|
|
|
|