schachnovelle 1.0.0-beta-6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +8 -4
- package/index.js +1 -39
- package/package.json +19 -3
- package/server/api.js +132 -0
- package/server/index.js +436 -11
- package/ui/Board.js +52 -16
- package/ui/Fen.js +5 -0
- package/ui/Game.js +648 -176
- package/ui/LichessGame.js +11 -0
- package/ui/Printer.js +27 -2
- package/ui/Side.js +91 -0
- package/ui/Utils.js +12 -2
- package/ui/index.js +91 -35
- package/ui/moves.js +0 -0
- package/views/error.pug +6 -0
- package/views/index.pug +6 -0
- package/views/layout.pug +6 -0
- package/views/welcome.pug +6 -0
- package/.env +0 -4
- package/lichess/index.js +0 -6
- package/tests/consoletest.js +0 -53
- package/tests/controller/index.js +0 -16
- package/tests/processes.js +0 -57
- package/yarn-error.log +0 -367
package/ui/Game.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
const readline = require(
|
|
2
|
-
const Side = require(
|
|
3
|
-
const Board = require(
|
|
4
|
-
const Printer = require(
|
|
5
|
-
const Utils = require(
|
|
6
|
-
const controller = require('../tests/controller')
|
|
1
|
+
const readline = require("readline")
|
|
2
|
+
const Side = require("./Side")
|
|
3
|
+
const Board = require("./Board")
|
|
4
|
+
const Printer = require("./Printer")
|
|
5
|
+
const Utils = require("./Utils")
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const Enquirer = require("enquirer")
|
|
9
8
|
|
|
10
9
|
class Game {
|
|
11
|
-
constructor
|
|
12
|
-
this.white = new Side(
|
|
13
|
-
this.black = new Side(
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.white = new Side("white")
|
|
12
|
+
this.black = new Side("black")
|
|
14
13
|
this.playingAs = options.playingAs
|
|
15
|
-
this.toMove =
|
|
14
|
+
this.toMove = "white"
|
|
16
15
|
|
|
17
16
|
this.moveIndex = 0
|
|
18
17
|
this.history = []
|
|
@@ -21,12 +20,13 @@ class Game {
|
|
|
21
20
|
|
|
22
21
|
// Initialize the board
|
|
23
22
|
this.board = new Board(this.white, this.black, {
|
|
24
|
-
mode:
|
|
23
|
+
mode: "pieces", // possibly add text mode
|
|
25
24
|
orientation: this.playingAs,
|
|
26
25
|
spacing: options.spacing,
|
|
27
|
-
mode: options.mode
|
|
26
|
+
mode: options.mode,
|
|
28
27
|
})
|
|
29
28
|
|
|
29
|
+
this.enquirer = new Enquirer()
|
|
30
30
|
this.printer = new Printer(options.spacing)
|
|
31
31
|
this.utils = new Utils()
|
|
32
32
|
}
|
|
@@ -36,21 +36,21 @@ class Game {
|
|
|
36
36
|
* 2. Sets that as the selected piece
|
|
37
37
|
*/
|
|
38
38
|
async promptPieceSelection() {
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
const response = await this.enquirer
|
|
40
|
+
.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: "select",
|
|
43
|
+
name: "piece",
|
|
44
|
+
message: "Piece",
|
|
45
|
+
choices: this.listAvailablePieces(),
|
|
46
|
+
result: result => {
|
|
47
|
+
this.setSelectedPiece(result)
|
|
48
|
+
return result
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
])
|
|
52
|
+
.catch(console.error)
|
|
53
|
+
|
|
54
54
|
/**
|
|
55
55
|
* Only do something if the await has been fulfilled
|
|
56
56
|
*/
|
|
@@ -64,24 +64,35 @@ class Game {
|
|
|
64
64
|
* 3. Asks for the new coordinates of that piece
|
|
65
65
|
*/
|
|
66
66
|
async promptFileSelection() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
const backToPiece = "... piece selection"
|
|
68
|
+
const response = await this.enquirer
|
|
69
|
+
.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: "select",
|
|
72
|
+
name: "file",
|
|
73
|
+
message: "File:",
|
|
74
|
+
choices: this.possibleFiles().concat(backToPiece),
|
|
75
|
+
result: result => {
|
|
76
|
+
if (result === backToPiece) {
|
|
77
|
+
return result
|
|
78
|
+
} else {
|
|
79
|
+
this.setSelectedFile(result)
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
.catch(console.error)
|
|
86
|
+
|
|
80
87
|
/**
|
|
81
88
|
* Only do something if the await has been fulfilled
|
|
82
89
|
*/
|
|
83
90
|
if (response) {
|
|
84
|
-
|
|
91
|
+
if (response.file === backToPiece) {
|
|
92
|
+
this.promptPieceSelection()
|
|
93
|
+
} else {
|
|
94
|
+
this.promptRankSelection()
|
|
95
|
+
}
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -90,126 +101,494 @@ class Game {
|
|
|
90
101
|
* 3. Asks for the new coordinates of that piece
|
|
91
102
|
*/
|
|
92
103
|
async promptRankSelection() {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const backToFile = "... file selection"
|
|
105
|
+
const response = await this.enquirer
|
|
106
|
+
.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: "select",
|
|
109
|
+
name: "rank",
|
|
110
|
+
message: "Rank:",
|
|
111
|
+
choices: this.possibleRanks().concat(backToFile),
|
|
112
|
+
result: result => {
|
|
113
|
+
if (result === backToFile) {
|
|
114
|
+
return result
|
|
115
|
+
} else {
|
|
116
|
+
this.setSelectedRank(Number(result))
|
|
117
|
+
return Number(result)
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
.catch(console.error)
|
|
123
|
+
|
|
106
124
|
/**
|
|
107
125
|
* Only do something if the await has been fulfilled
|
|
108
126
|
*/
|
|
109
127
|
if (response) {
|
|
110
|
-
|
|
128
|
+
if (response.rank === backToFile) {
|
|
129
|
+
this.promptFileSelection()
|
|
130
|
+
} else {
|
|
131
|
+
this.validateMove()
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
/**
|
|
137
|
+
* For the given color, return all possible moves
|
|
138
|
+
*/
|
|
139
|
+
allPossibleMoves(color) {
|
|
140
|
+
let all = []
|
|
141
|
+
this.board[color].pieces
|
|
142
|
+
.filter(p => !p.captured)
|
|
143
|
+
.forEach(p => {
|
|
144
|
+
this.possibleMoves(p, color)
|
|
145
|
+
.forEach(m => all.push(m))
|
|
146
|
+
})
|
|
147
|
+
return all
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Return all possible checks
|
|
152
|
+
* @param {*} color
|
|
153
|
+
* @returns
|
|
154
|
+
*/
|
|
155
|
+
allPossibleChecks(color) {
|
|
156
|
+
// Moves
|
|
157
|
+
const moves = this.allPossibleMoves(color)
|
|
158
|
+
|
|
159
|
+
const allChecks = moves.filter(m => m[2] === 'check')
|
|
160
|
+
|
|
161
|
+
return allChecks
|
|
122
162
|
}
|
|
123
163
|
|
|
124
164
|
/**
|
|
125
165
|
* Return the possible moves for the selected piece
|
|
126
166
|
* Returns an object with all possible moves split into file, and rank
|
|
127
167
|
*/
|
|
128
|
-
possibleMoves
|
|
168
|
+
possibleMoves(piece, color) {
|
|
169
|
+
// TODO: If the current color is in check,
|
|
170
|
+
// then limit the possible moves to the ones that get you out of it
|
|
171
|
+
|
|
129
172
|
// Start with the selected piece. First of all, it can't move to it's own square
|
|
130
|
-
// console.log(this.getSelectedPiece())
|
|
131
173
|
switch (piece.type) {
|
|
132
|
-
case
|
|
133
|
-
return this.possiblePawnMoves(piece)
|
|
134
|
-
case
|
|
135
|
-
return this.
|
|
136
|
-
case
|
|
137
|
-
return this.
|
|
138
|
-
case
|
|
139
|
-
return this.
|
|
140
|
-
case
|
|
141
|
-
return this.
|
|
142
|
-
case
|
|
143
|
-
return this.
|
|
174
|
+
case "Pawn":
|
|
175
|
+
return this.possiblePawnMoves(piece, color)
|
|
176
|
+
case "Knight":
|
|
177
|
+
return this.possibleKnightMoves(piece, color)
|
|
178
|
+
case "Bishop":
|
|
179
|
+
return this.possibleBishopMoves(piece, color)
|
|
180
|
+
case "Rook":
|
|
181
|
+
return this.possibleRookMoves(piece, color)
|
|
182
|
+
case "Queen":
|
|
183
|
+
return this.possibleQueenMoves(piece, color)
|
|
184
|
+
case "King":
|
|
185
|
+
return this.possibleKingMoves(piece, color)
|
|
144
186
|
}
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
/**
|
|
148
190
|
* Returns all possible moves for a pawn
|
|
149
191
|
*/
|
|
150
|
-
possiblePawnMoves (pawn) {
|
|
192
|
+
possiblePawnMoves (pawn, color) {
|
|
151
193
|
const possible = []
|
|
152
194
|
const positions = []
|
|
153
|
-
const
|
|
154
|
-
|
|
195
|
+
const direction = this.playingAs === color ? 1 : -1
|
|
196
|
+
|
|
197
|
+
const nextRank = pawn.pos[1] + direction
|
|
198
|
+
const secondNextRank = pawn.pos[1] + (direction * 2)
|
|
155
199
|
// If it's on the starting rank, allow two moves
|
|
156
|
-
if (pawn.pos[1] === 2 &&
|
|
157
|
-
positions.push(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
positions.push(
|
|
163
|
-
[pawn.pos[0], nextRank],
|
|
164
|
-
[pawn.pos[0], secondNextRank]
|
|
165
|
-
)
|
|
166
|
-
} else if (this.toMove === 'white') {
|
|
167
|
-
positions.push(
|
|
168
|
-
[pawn.pos[0], nextRank]
|
|
169
|
-
)
|
|
200
|
+
if (pawn.pos[1] === 2 && color === "white") {
|
|
201
|
+
positions.push([pawn.pos[0], nextRank], [pawn.pos[0], secondNextRank])
|
|
202
|
+
} else if (pawn.pos[1] === 7 && color === "black") {
|
|
203
|
+
positions.push([pawn.pos[0], nextRank], [pawn.pos[0], secondNextRank])
|
|
204
|
+
} else if (color === "white") {
|
|
205
|
+
positions.push([pawn.pos[0], nextRank])
|
|
170
206
|
} else {
|
|
171
|
-
positions.push(
|
|
172
|
-
[pawn.pos[0], nextRank]
|
|
173
|
-
)
|
|
207
|
+
positions.push([pawn.pos[0], nextRank])
|
|
174
208
|
}
|
|
175
209
|
// Captures
|
|
176
210
|
const fileIndex = this.board.files.indexOf(pawn.pos[0])
|
|
177
211
|
const positionsToCapture = []
|
|
178
212
|
switch (fileIndex) {
|
|
179
|
-
case
|
|
180
|
-
const posXL = [
|
|
213
|
+
case 0:
|
|
214
|
+
const posXL = [this.board.files[fileIndex + 1], nextRank]
|
|
181
215
|
positionsToCapture.push(posXL)
|
|
182
216
|
break
|
|
183
|
-
case
|
|
184
|
-
const posXR = [
|
|
217
|
+
case 7:
|
|
218
|
+
const posXR = [this.board.files[fileIndex - 1], nextRank]
|
|
185
219
|
positionsToCapture.push(posXR)
|
|
186
220
|
break
|
|
187
221
|
default:
|
|
188
|
-
const posL = [
|
|
189
|
-
const posR = [
|
|
222
|
+
const posL = [this.board.files[fileIndex + 1], nextRank]
|
|
223
|
+
const posR = [this.board.files[fileIndex - 1], nextRank]
|
|
190
224
|
positionsToCapture.push(posL, posR)
|
|
191
225
|
break
|
|
192
226
|
}
|
|
193
227
|
for (const pos of positions) {
|
|
194
|
-
if (this.movePotential(pos, false) !==
|
|
228
|
+
if (this.movePotential(pos, color, false) !== "blocked") {
|
|
195
229
|
possible.push(pos)
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
232
|
for (const pos of positionsToCapture) {
|
|
199
|
-
if (this.movePotential(pos, true) ===
|
|
233
|
+
if (this.movePotential(pos, color, true) === "capture") {
|
|
234
|
+
possible.push(pos)
|
|
235
|
+
} else if (this.movePotential(pos, color, true) === "check") {
|
|
236
|
+
pos.push('check')
|
|
200
237
|
possible.push(pos)
|
|
201
238
|
}
|
|
202
239
|
}
|
|
203
240
|
// TODO: en passant (depends on a history)
|
|
204
|
-
|
|
241
|
+
return possible
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Just returns the candidate positions a knight could move to
|
|
246
|
+
* @param {Integer} fileIndex
|
|
247
|
+
* @param {Integer} rankIndex
|
|
248
|
+
*/
|
|
249
|
+
knightCandidateMoves(fileIndex, rankIndex) {
|
|
250
|
+
return [
|
|
251
|
+
// Two up, one left
|
|
252
|
+
[this.board.files[fileIndex - 1], this.board.ranks[rankIndex + 2]],
|
|
253
|
+
// Two up, one right
|
|
254
|
+
[this.board.files[fileIndex + 1], this.board.ranks[rankIndex + 2]],
|
|
255
|
+
// Two right, one up
|
|
256
|
+
[this.board.files[fileIndex + 2], this.board.ranks[rankIndex + 1]],
|
|
257
|
+
// Two right, one down
|
|
258
|
+
[this.board.files[fileIndex + 2], this.board.ranks[rankIndex - 1]],
|
|
259
|
+
// Two down, one right
|
|
260
|
+
[this.board.files[fileIndex + 1], this.board.ranks[rankIndex - 2]],
|
|
261
|
+
// Two down, one left
|
|
262
|
+
[this.board.files[fileIndex - 1], this.board.ranks[rankIndex - 2]],
|
|
263
|
+
// Two left, one down
|
|
264
|
+
[this.board.files[fileIndex - 2], this.board.ranks[rankIndex - 1]],
|
|
265
|
+
// Two left, one up
|
|
266
|
+
[this.board.files[fileIndex - 2], this.board.ranks[rankIndex + 1]],
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Returns the possible moves for a knight
|
|
272
|
+
*/
|
|
273
|
+
possibleKnightMoves(knight, color) {
|
|
274
|
+
const fileIndex = this.board.files.indexOf(knight.pos[0])
|
|
275
|
+
const rankIndex = this.board.ranks.indexOf(knight.pos[1])
|
|
276
|
+
// From the starting square, draw a plus in each direction and then go one diagonal from there
|
|
277
|
+
// Take the starting file and go left and right
|
|
278
|
+
const candidates = this.knightCandidateMoves(fileIndex, rankIndex)
|
|
279
|
+
|
|
280
|
+
const possible = candidates.filter(move => {
|
|
281
|
+
if (move[0] !== undefined && move[1] !== undefined) {
|
|
282
|
+
if (this.movePotential(move, color, true) === "check") {
|
|
283
|
+
move.push('check')
|
|
284
|
+
return move
|
|
285
|
+
} else if (this.movePotential(move, color, true) !== "blocked") {
|
|
286
|
+
return move
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
return possible
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Returns the possible moves for a bishop
|
|
295
|
+
*/
|
|
296
|
+
possibleBishopMoves(bishop, color) {
|
|
297
|
+
// go into all diagonal directions until there's no place or there's a capture or there's a block
|
|
298
|
+
const fileIndex = this.board.files.indexOf(bishop.pos[0])
|
|
299
|
+
const rankIndex = this.board.ranks.indexOf(bishop.pos[1])
|
|
300
|
+
|
|
301
|
+
const fU = this.forwardAndUp(fileIndex, rankIndex, color)
|
|
302
|
+
const fD = this.forwardAndDown(fileIndex, rankIndex, color, false)
|
|
303
|
+
const bU = this.backwardAndUp(fileIndex, rankIndex, color, false)
|
|
304
|
+
const bD = this.backwardAndDown(fileIndex, rankIndex, color, false)
|
|
305
|
+
|
|
306
|
+
return [...fU, ...fD, ...bU, ...bD]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Returns the possible moves for a rook
|
|
311
|
+
*/
|
|
312
|
+
possibleRookMoves(rook, color) {
|
|
313
|
+
// go into all diagonal directions until there's no place or there's a capture or there's a block
|
|
314
|
+
const fileIndex = this.board.files.indexOf(rook.pos[0])
|
|
315
|
+
const rankIndex = this.board.ranks.indexOf(rook.pos[1])
|
|
316
|
+
|
|
317
|
+
const u = this.up(fileIndex, rankIndex, color, false)
|
|
318
|
+
const d = this.down(fileIndex, rankIndex, color, false)
|
|
319
|
+
const l = this.left(fileIndex, rankIndex, color, false)
|
|
320
|
+
const r = this.right(fileIndex, rankIndex, color, false)
|
|
321
|
+
|
|
322
|
+
return [...u, ...d, ...l, ...r]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Returns the possible moves for a queen
|
|
327
|
+
*/
|
|
328
|
+
possibleQueenMoves(queen, color) {
|
|
329
|
+
// go into all diagonal directions until there's no place or there's a capture or there's a block
|
|
330
|
+
const fileIndex = this.board.files.indexOf(queen.pos[0])
|
|
331
|
+
const rankIndex = this.board.ranks.indexOf(queen.pos[1])
|
|
332
|
+
|
|
333
|
+
const u = this.up(fileIndex, rankIndex, color, false)
|
|
334
|
+
const d = this.down(fileIndex, rankIndex, color, false)
|
|
335
|
+
const l = this.left(fileIndex, rankIndex, color, false)
|
|
336
|
+
const r = this.right(fileIndex, rankIndex, color, false)
|
|
337
|
+
const fU = this.forwardAndUp(fileIndex, rankIndex, color, false)
|
|
338
|
+
const fD = this.forwardAndDown(fileIndex, rankIndex, color, false)
|
|
339
|
+
const bU = this.backwardAndUp(fileIndex, rankIndex, color, false)
|
|
340
|
+
const bD = this.backwardAndDown(fileIndex, rankIndex, color, false)
|
|
341
|
+
|
|
342
|
+
return [...u, ...d, ...l, ...r, ...fU, ...fD, ...bU, ...bD]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Returns the possible moves for a queen
|
|
347
|
+
*/
|
|
348
|
+
possibleKingMoves(king, color) {
|
|
349
|
+
// go into all diagonal directions until there's no place or there's a capture or there's a block
|
|
350
|
+
const fileIndex = this.board.files.indexOf(king.pos[0])
|
|
351
|
+
const rankIndex = this.board.ranks.indexOf(king.pos[1])
|
|
352
|
+
|
|
353
|
+
const u = this.up(fileIndex, rankIndex, color, true)
|
|
354
|
+
const d = this.down(fileIndex, rankIndex, color, true)
|
|
355
|
+
const l = this.left(fileIndex, rankIndex, color, true)
|
|
356
|
+
const r = this.right(fileIndex, rankIndex, color, true)
|
|
357
|
+
const fU = this.forwardAndUp(fileIndex, rankIndex, color, true)
|
|
358
|
+
const fD = this.forwardAndDown(fileIndex, rankIndex, color, true)
|
|
359
|
+
const bU = this.backwardAndUp(fileIndex, rankIndex, color, true)
|
|
360
|
+
const bD = this.backwardAndDown(fileIndex, rankIndex, color, true)
|
|
361
|
+
|
|
362
|
+
return [...u, ...d, ...l, ...r, ...fU, ...fD, ...bU, ...bD]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns the possible moves on a diagonal going right and up
|
|
367
|
+
* This function is used in two contexts;
|
|
368
|
+
* 1. To determine the next moves for a person playing
|
|
369
|
+
* 2. To check if the next move will be checking the opponent's king
|
|
370
|
+
*/
|
|
371
|
+
forwardAndUp(fileIndex, rankIndex, color, king) {
|
|
372
|
+
const possible = []
|
|
373
|
+
let fi = fileIndex + 1
|
|
374
|
+
let ri = rankIndex + 1
|
|
375
|
+
|
|
376
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
377
|
+
|
|
378
|
+
while (
|
|
379
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
380
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
381
|
+
) {
|
|
382
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
383
|
+
nextPosition.push('check')
|
|
384
|
+
possible.push(nextPosition)
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
possible.push(nextPosition)
|
|
388
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
389
|
+
fi++
|
|
390
|
+
ri++
|
|
391
|
+
nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return possible
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Returns the possible moves on a diagonal going right and down
|
|
399
|
+
*/
|
|
400
|
+
forwardAndDown(fileIndex, rankIndex, color, king) {
|
|
401
|
+
const possible = []
|
|
402
|
+
let fi = fileIndex + 1
|
|
403
|
+
let ri = rankIndex + -1
|
|
404
|
+
|
|
405
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
406
|
+
|
|
407
|
+
while (
|
|
408
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
409
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
410
|
+
) {
|
|
411
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
412
|
+
nextPosition.push('check')
|
|
413
|
+
possible.push(nextPosition)
|
|
414
|
+
break
|
|
415
|
+
}
|
|
416
|
+
possible.push(nextPosition)
|
|
417
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
418
|
+
fi++
|
|
419
|
+
ri--
|
|
420
|
+
nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
421
|
+
}
|
|
422
|
+
return possible
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Returns the possible moves on a diagonal going left and up
|
|
427
|
+
*/
|
|
428
|
+
backwardAndUp(fileIndex, rankIndex, color, king) {
|
|
429
|
+
const possible = []
|
|
430
|
+
let fi = fileIndex - 1
|
|
431
|
+
let ri = rankIndex + 1
|
|
432
|
+
|
|
433
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
434
|
+
|
|
435
|
+
while (
|
|
436
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
437
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
438
|
+
) {
|
|
439
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
440
|
+
nextPosition.push('check')
|
|
441
|
+
possible.push(nextPosition)
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
possible.push(nextPosition)
|
|
445
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
446
|
+
fi--
|
|
447
|
+
ri++
|
|
448
|
+
nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
449
|
+
}
|
|
450
|
+
return possible
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Returns the possible moves on a diagonal going left and down
|
|
455
|
+
*/
|
|
456
|
+
backwardAndDown(fileIndex, rankIndex, color, king) {
|
|
457
|
+
const possible = []
|
|
458
|
+
let fi = fileIndex - 1
|
|
459
|
+
let ri = rankIndex - 1
|
|
460
|
+
|
|
461
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
462
|
+
|
|
463
|
+
while (
|
|
464
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
465
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
466
|
+
) {
|
|
467
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
468
|
+
nextPosition.push('check')
|
|
469
|
+
possible.push(nextPosition)
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
possible.push(nextPosition)
|
|
473
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
474
|
+
fi--
|
|
475
|
+
ri--
|
|
476
|
+
nextPosition = [this.board.files[fi], this.board.ranks[ri]]
|
|
477
|
+
}
|
|
478
|
+
return possible
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Returns the possible moves on a file going up
|
|
483
|
+
*/
|
|
484
|
+
up(fileIndex, rankIndex, color, king) {
|
|
485
|
+
const possible = []
|
|
486
|
+
let ri = rankIndex + 1
|
|
487
|
+
|
|
488
|
+
let nextPosition = [this.board.files[fileIndex], this.board.ranks[ri]]
|
|
489
|
+
|
|
490
|
+
while (
|
|
491
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
492
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
493
|
+
) {
|
|
494
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
495
|
+
nextPosition.push('check')
|
|
496
|
+
possible.push(nextPosition)
|
|
497
|
+
break
|
|
498
|
+
}
|
|
499
|
+
possible.push(nextPosition)
|
|
500
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
501
|
+
ri++
|
|
502
|
+
nextPosition = [this.board.files[fileIndex], this.board.ranks[ri]]
|
|
503
|
+
}
|
|
504
|
+
return possible
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Returns the possible moves on a file going down
|
|
509
|
+
*/
|
|
510
|
+
down(fileIndex, rankIndex, color, king) {
|
|
511
|
+
const possible = []
|
|
512
|
+
let ri = rankIndex - 1
|
|
513
|
+
|
|
514
|
+
let nextPosition = [this.board.files[fileIndex], this.board.ranks[ri]]
|
|
515
|
+
|
|
516
|
+
while (
|
|
517
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
518
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
519
|
+
) {
|
|
520
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
521
|
+
nextPosition.push('check')
|
|
522
|
+
possible.push(nextPosition)
|
|
523
|
+
break
|
|
524
|
+
}
|
|
525
|
+
possible.push(nextPosition)
|
|
526
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
527
|
+
ri--
|
|
528
|
+
nextPosition = [this.board.files[fileIndex], this.board.ranks[ri]]
|
|
529
|
+
}
|
|
530
|
+
return possible
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Returns the possible moves on a rank going left
|
|
535
|
+
*/
|
|
536
|
+
left(fileIndex, rankIndex, color, king) {
|
|
537
|
+
const possible = []
|
|
538
|
+
let fi = fileIndex - 1
|
|
539
|
+
|
|
540
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[rankIndex]]
|
|
541
|
+
|
|
542
|
+
while (
|
|
543
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
544
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
545
|
+
) {
|
|
546
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
547
|
+
nextPosition.push('check')
|
|
548
|
+
possible.push(nextPosition)
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
possible.push(nextPosition)
|
|
552
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
553
|
+
fi--
|
|
554
|
+
nextPosition = [this.board.files[fi], this.board.ranks[rankIndex]]
|
|
555
|
+
}
|
|
556
|
+
return possible
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Returns the possible moves on a rank going right
|
|
561
|
+
*/
|
|
562
|
+
right(fileIndex, rankIndex, color, king) {
|
|
563
|
+
const possible = []
|
|
564
|
+
let fi = fileIndex + 1
|
|
565
|
+
|
|
566
|
+
let nextPosition = [this.board.files[fi], this.board.ranks[rankIndex]]
|
|
567
|
+
|
|
568
|
+
while (
|
|
569
|
+
this.movePotential(nextPosition, color, true) !== "blocked" &&
|
|
570
|
+
this.movePotential(nextPosition, color, true) !== "impossible"
|
|
571
|
+
) {
|
|
572
|
+
if (this.movePotential(nextPosition, color, true) === "check") {
|
|
573
|
+
nextPosition.push('check')
|
|
574
|
+
possible.push(nextPosition)
|
|
575
|
+
break
|
|
576
|
+
}
|
|
577
|
+
possible.push(nextPosition)
|
|
578
|
+
if (this.movePotential(nextPosition, color, true) === "capture" || king) break
|
|
579
|
+
fi++
|
|
580
|
+
nextPosition = [this.board.files[fi], this.board.ranks[rankIndex]]
|
|
581
|
+
}
|
|
205
582
|
return possible
|
|
206
583
|
}
|
|
207
584
|
|
|
208
585
|
/**
|
|
209
586
|
* Returns the possible ranks from the possiblemoves function
|
|
210
587
|
*/
|
|
211
|
-
possibleFiles
|
|
212
|
-
const possible = this.possibleMoves(this.selectedPiece).map(
|
|
588
|
+
possibleFiles() {
|
|
589
|
+
const possible = this.possibleMoves(this.selectedPiece, this.toMove).map(pos => {
|
|
590
|
+
return pos[0]
|
|
591
|
+
})
|
|
213
592
|
/** Filter unique */
|
|
214
593
|
return [...new Set(possible)]
|
|
215
594
|
}
|
|
@@ -217,52 +596,70 @@ class Game {
|
|
|
217
596
|
/**
|
|
218
597
|
* Returns the possible ranks from the possiblemoves function
|
|
219
598
|
*/
|
|
220
|
-
possibleRanks
|
|
221
|
-
const possible = this.possibleMoves(this.selectedPiece
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
599
|
+
possibleRanks() {
|
|
600
|
+
const possible = this.possibleMoves(this.selectedPiece, this.toMove)
|
|
601
|
+
.filter(move => {
|
|
602
|
+
if (move[0] === this.selectedFile) {
|
|
603
|
+
return move[1]
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
.map(pos => {
|
|
607
|
+
return pos[1].toString()
|
|
608
|
+
})
|
|
226
609
|
/** Filter unique */
|
|
227
610
|
return [...new Set(possible)]
|
|
228
611
|
}
|
|
229
612
|
|
|
230
613
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
614
|
+
* 1. Process captures
|
|
615
|
+
* 2. TODO: Check check
|
|
616
|
+
* 3. TODO: Check checkmate
|
|
233
617
|
*/
|
|
234
|
-
validateMove
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.processCapture()
|
|
240
|
-
this.commitMove()
|
|
241
|
-
// Opponent to move
|
|
242
|
-
} else {
|
|
243
|
-
// retry
|
|
244
|
-
console.error('Not a legal move. Try again')
|
|
245
|
-
this.runPrompt()
|
|
246
|
-
}
|
|
618
|
+
async validateMove() {
|
|
619
|
+
// Commit the move to update the board
|
|
620
|
+
this.processCapture()
|
|
621
|
+
await this.promptPromotion()
|
|
622
|
+
this.commitMove()
|
|
247
623
|
}
|
|
248
624
|
|
|
249
625
|
/**
|
|
250
626
|
* Allowed configures the check to consider if a capture may be made
|
|
627
|
+
* If it is true,
|
|
251
628
|
* Returns 'capture' if there is an opposing piece at the given position
|
|
629
|
+
* Returns 'check' if the opposing piece at the given position is a king
|
|
252
630
|
* Returns 'blocked' if there is an own piece or 'possible' for no piece at the given position
|
|
253
631
|
*/
|
|
254
|
-
movePotential
|
|
255
|
-
|
|
256
|
-
|
|
632
|
+
movePotential(pos, color, allowed) {
|
|
633
|
+
if (pos[0] === undefined || pos[1] === undefined) {
|
|
634
|
+
return "impossible"
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const opponentsPieces =
|
|
638
|
+
this.board[this.utils.opposingColor(color)].pieces
|
|
639
|
+
|
|
640
|
+
const ownPiece = this.board[color].pieces.find(piece => {
|
|
257
641
|
return piece.pos[0] === pos[0] && piece.pos[1] === pos[1]
|
|
258
642
|
})
|
|
259
|
-
const opponentsPiece = opponentsPieces.find(
|
|
643
|
+
const opponentsPiece = opponentsPieces.find(piece => {
|
|
260
644
|
return piece.pos[0] === pos[0] && piece.pos[1] === pos[1]
|
|
261
645
|
})
|
|
262
646
|
|
|
263
|
-
if (opponentsPiece && allowed) {
|
|
264
|
-
|
|
265
|
-
|
|
647
|
+
if (opponentsPiece && allowed) {
|
|
648
|
+
if (opponentsPiece.type === "King") {
|
|
649
|
+
return "check"
|
|
650
|
+
} else {
|
|
651
|
+
return "capture"
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (ownPiece && allowed) {
|
|
655
|
+
return "blocked"
|
|
656
|
+
}
|
|
657
|
+
if (ownPiece || (opponentsPiece && !allowed)) {
|
|
658
|
+
return "blocked"
|
|
659
|
+
}
|
|
660
|
+
if (!ownPiece && !opponentsPiece) {
|
|
661
|
+
return "possible"
|
|
662
|
+
}
|
|
266
663
|
}
|
|
267
664
|
|
|
268
665
|
/**
|
|
@@ -270,16 +667,84 @@ class Game {
|
|
|
270
667
|
* 1. Checks for capture
|
|
271
668
|
* 2. If capture, set the piece status to captured
|
|
272
669
|
*/
|
|
273
|
-
processCapture
|
|
274
|
-
const opponentsPieces =
|
|
275
|
-
|
|
276
|
-
|
|
670
|
+
processCapture() {
|
|
671
|
+
const opponentsPieces =
|
|
672
|
+
this.board[this.utils.opposingColor(this.toMove)].pieces
|
|
673
|
+
const capturedPiece = opponentsPieces.find(piece => {
|
|
674
|
+
return (
|
|
675
|
+
piece.pos[0] === this.selectedFile && piece.pos[1] === this.selectedRank
|
|
676
|
+
)
|
|
277
677
|
})
|
|
278
678
|
if (capturedPiece) {
|
|
279
679
|
capturedPiece.captured = true
|
|
280
680
|
}
|
|
281
681
|
}
|
|
282
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Prompt promotion
|
|
685
|
+
* 1. Asks user which piece to promote to
|
|
686
|
+
* 2. Sets said piece to its new values
|
|
687
|
+
*/
|
|
688
|
+
async promptPromotion() {
|
|
689
|
+
if (
|
|
690
|
+
(this.selectedPiece.type === "Pawn" &&
|
|
691
|
+
this.toMove === "white" &&
|
|
692
|
+
this.selectedRank === 8) ||
|
|
693
|
+
(this.selectedPiece.type === "Pawn" &&
|
|
694
|
+
this.toMove === "black" &&
|
|
695
|
+
this.selectedRank === 1)
|
|
696
|
+
) {
|
|
697
|
+
await this.enquirer
|
|
698
|
+
.prompt({
|
|
699
|
+
type: "select",
|
|
700
|
+
name: "promotion",
|
|
701
|
+
message: "Promote to:",
|
|
702
|
+
choices: this.board[this.toMove].promotionChoices.map(choice => {
|
|
703
|
+
return choice.piece
|
|
704
|
+
}),
|
|
705
|
+
result: piece => {
|
|
706
|
+
const promotionChoice = this.board[
|
|
707
|
+
this.toMove
|
|
708
|
+
].promotionChoices.find(choice => {
|
|
709
|
+
return choice.piece === piece
|
|
710
|
+
})
|
|
711
|
+
const selectedPiece = this.getSelectedPiece()
|
|
712
|
+
selectedPiece.piece = piece
|
|
713
|
+
selectedPiece.type = promotionChoice.type
|
|
714
|
+
selectedPiece.prefix = promotionChoice.prefix
|
|
715
|
+
selectedPiece.value = promotionChoice.piece
|
|
716
|
+
return selectedPiece
|
|
717
|
+
},
|
|
718
|
+
})
|
|
719
|
+
.catch(console.error)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Check master function. Is run right before passTurn and board.render
|
|
725
|
+
*/
|
|
726
|
+
checkForCheck() {
|
|
727
|
+
// If the opponent's checks length is > 0, that means you're in check
|
|
728
|
+
const opponent = this.utils.opposingColor(this.toMove)
|
|
729
|
+
const opponentsChecks = this.allPossibleChecks(opponent)
|
|
730
|
+
|
|
731
|
+
if (opponentsChecks.length > 0) {
|
|
732
|
+
this.board[this.toMove].inCheck = true
|
|
733
|
+
} else {
|
|
734
|
+
this.board[this.toMove].inCheck = false
|
|
735
|
+
this.board[opponent].inCheck = false
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Passes the turn to the other player by setting the toMove var and prompting the next move
|
|
741
|
+
*/
|
|
742
|
+
passTurn() {
|
|
743
|
+
this.toMove = this.toMove === "white" ? "black" : "white"
|
|
744
|
+
this.checkForCheck()
|
|
745
|
+
this.promptPieceSelection()
|
|
746
|
+
}
|
|
747
|
+
|
|
283
748
|
/**
|
|
284
749
|
* Commits the given move
|
|
285
750
|
* 1. Sets the position of the selected piece
|
|
@@ -287,53 +752,37 @@ class Game {
|
|
|
287
752
|
* 3. Passes the turn to the other player
|
|
288
753
|
* 4. Renders the board
|
|
289
754
|
*/
|
|
290
|
-
commitMove
|
|
755
|
+
commitMove() {
|
|
291
756
|
this.getSelectedPiece().pos = [this.selectedFile, this.selectedRank]
|
|
292
757
|
this.commitToHistory()
|
|
293
758
|
this.passTurn()
|
|
759
|
+
this.board.clear()
|
|
294
760
|
this.board.render()
|
|
295
761
|
}
|
|
296
762
|
|
|
297
|
-
/**
|
|
298
|
-
* Passes the turn to the other player by setting the toMove var and prompting the next move
|
|
299
|
-
*/
|
|
300
|
-
passTurn () {
|
|
301
|
-
this.toMove === 'white' ? this.toMove = 'black' : this.toMove = 'white'
|
|
302
|
-
this.promptPieceSelection()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
763
|
/**
|
|
306
764
|
* Pushes move to history
|
|
307
765
|
*/
|
|
308
|
-
commitToHistory
|
|
309
|
-
console.log(`${this.selectedPiece.piece} to ${this.selectedFile}${this.selectedRank}`)
|
|
310
|
-
// if (this.toMove === 'white') {
|
|
311
|
-
// this.history.push({
|
|
312
|
-
// turn: this.moveIndex + 1,
|
|
313
|
-
// move: move
|
|
314
|
-
// })
|
|
315
|
-
// } else {
|
|
316
|
-
// this.history[this.moveIndex].move += `, ${move}`
|
|
317
|
-
// this.moveIndex++
|
|
318
|
-
// }
|
|
319
|
-
}
|
|
766
|
+
commitToHistory() {}
|
|
320
767
|
|
|
321
768
|
/**
|
|
322
769
|
* Returns a list of available pieces for the prompt
|
|
323
770
|
*/
|
|
324
|
-
listAvailablePieces
|
|
325
|
-
return this.board[this.toMove].pieces
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
771
|
+
listAvailablePieces() {
|
|
772
|
+
return this.board[this.toMove].pieces
|
|
773
|
+
.filter(piece => {
|
|
774
|
+
return !piece.captured && this.possibleMoves(piece, this.toMove).length > 0
|
|
775
|
+
})
|
|
776
|
+
.map(piece => {
|
|
777
|
+
return `${piece.piece} (${piece.pos})`
|
|
778
|
+
})
|
|
330
779
|
}
|
|
331
780
|
|
|
332
781
|
/**
|
|
333
782
|
* Sets the given piece as selected for this move
|
|
334
783
|
*/
|
|
335
|
-
setSelectedPiece
|
|
336
|
-
const selection = this.board[this.toMove].pieces.find(
|
|
784
|
+
setSelectedPiece(piece) {
|
|
785
|
+
const selection = this.board[this.toMove].pieces.find(p => {
|
|
337
786
|
return p.pos[0] === piece[3] && p.pos[1] === Number(piece[5])
|
|
338
787
|
})
|
|
339
788
|
this.selectedPiece = selection
|
|
@@ -342,31 +791,54 @@ class Game {
|
|
|
342
791
|
/**
|
|
343
792
|
* Sets the given file as selected for this move
|
|
344
793
|
*/
|
|
345
|
-
setSelectedFile
|
|
794
|
+
setSelectedFile(file) {
|
|
346
795
|
this.selectedFile = file
|
|
347
796
|
}
|
|
348
797
|
|
|
349
798
|
/**
|
|
350
799
|
* Sets the given rank as selected for this move
|
|
351
800
|
*/
|
|
352
|
-
setSelectedRank
|
|
801
|
+
setSelectedRank(rank) {
|
|
353
802
|
this.selectedRank = rank
|
|
354
803
|
}
|
|
355
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Get the current player's piece
|
|
807
|
+
*/
|
|
808
|
+
getPiece(type) {
|
|
809
|
+
return this.board[this.toMove].pieces.find(piece => {
|
|
810
|
+
return piece.type === type
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Get the opponent's piece
|
|
816
|
+
*/
|
|
817
|
+
getOpponentsPiece(type) {
|
|
818
|
+
return this.board[this.utils.opposingColor(this.toMove)].pieces.find(
|
|
819
|
+
piece => {
|
|
820
|
+
return piece.type === type
|
|
821
|
+
}
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
356
825
|
/**
|
|
357
826
|
* returns the selected piece for this move
|
|
358
827
|
*/
|
|
359
|
-
getSelectedPiece
|
|
360
|
-
return this.board[this.toMove].pieces.find(piece => {
|
|
828
|
+
getSelectedPiece() {
|
|
829
|
+
return this.board[this.toMove].pieces.find(piece => {
|
|
830
|
+
return piece === this.selectedPiece
|
|
831
|
+
})
|
|
361
832
|
}
|
|
362
833
|
|
|
363
834
|
/**
|
|
364
835
|
* Um Gottes Wille, nicht!
|
|
365
836
|
*/
|
|
366
|
-
init
|
|
837
|
+
init() {
|
|
838
|
+
this.board.clear()
|
|
367
839
|
this.board.render()
|
|
368
840
|
this.promptPieceSelection()
|
|
369
841
|
}
|
|
370
842
|
}
|
|
371
843
|
|
|
372
|
-
module.exports = Game
|
|
844
|
+
module.exports = Game
|