Commit | Line | Data |
---|---|---|
d982fffc BA |
1 | import { ChessRules, Move, PiPo } from "@/base_rules"; |
2 | import { randInt } from "@/utils/alea"; | |
3 | import { ArrayFun } from "@/utils/array"; | |
d2af3400 | 4 | |
d982fffc | 5 | export class EmergoRules extends ChessRules { |
d2af3400 | 6 | |
d982fffc BA |
7 | // Simple encoding: A to L = 1 to 12, from left to right, if white controls. |
8 | // Lowercase if black controls. | |
9 | // Single piece (no prisoners): A@ to L@ (+ lowercase) | |
10 | ||
11 | static get HasFlags() { | |
12 | return false; | |
13 | } | |
14 | ||
15 | static get HasEnpassant() { | |
16 | return false; | |
17 | } | |
18 | ||
19 | static get DarkBottomRight() { | |
20 | return true; | |
21 | } | |
22 | ||
23 | // board element == file name: | |
24 | static board2fen(b) { | |
25 | return b; | |
26 | } | |
27 | static fen2board(f) { | |
28 | return f; | |
29 | } | |
30 | ||
31 | static IsGoodPosition(position) { | |
32 | if (position.length == 0) return false; | |
33 | const rows = position.split("/"); | |
34 | if (rows.length != V.size.x) return false; | |
35 | for (let row of rows) { | |
36 | let sumElts = 0; | |
37 | for (let i = 0; i < row.length; i++) { | |
38 | // Add only 0.5 per symbol because 2 per piece | |
39 | if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5; | |
40 | else { | |
41 | const num = parseInt(row[i], 10); | |
42 | if (isNaN(num) || num <= 0) return false; | |
43 | sumElts += num; | |
44 | } | |
45 | } | |
46 | if (sumElts != V.size.y) return false; | |
47 | } | |
48 | return true; | |
49 | } | |
50 | ||
51 | static GetBoard(position) { | |
52 | const rows = position.split("/"); | |
53 | let board = ArrayFun.init(V.size.x, V.size.y, ""); | |
54 | for (let i = 0; i < rows.length; i++) { | |
55 | let j = 0; | |
56 | for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) { | |
57 | const character = rows[i][indexInRow]; | |
58 | const num = parseInt(character, 10); | |
59 | // If num is a number, just shift j: | |
60 | if (!isNaN(num)) j += num; | |
61 | else | |
62 | // Something at position i,j | |
63 | board[i][j++] = V.fen2board(character + rows[i][++indexInRow]); | |
64 | } | |
65 | } | |
66 | return board; | |
67 | } | |
68 | ||
69 | getPpath(b) { | |
70 | return "Emergo/" + b; | |
71 | } | |
72 | ||
73 | getColor(x, y) { | |
74 | if (x >= V.size.x) return x == V.size.x ? "w" : "b"; | |
75 | if (this.board[x][y].charCodeAt(0) < 97) return 'w'; | |
76 | return 'b'; | |
77 | } | |
78 | ||
79 | getPiece() { | |
80 | return V.PAWN; //unused | |
81 | } | |
82 | ||
83 | static IsGoodFen(fen) { | |
84 | if (!ChessRules.IsGoodFen(fen)) return false; | |
85 | const fenParsed = V.ParseFen(fen); | |
86 | // 3) Check reserves | |
87 | if ( | |
88 | !fenParsed.reserve || | |
89 | !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/) | |
90 | ) { | |
91 | return false; | |
92 | } | |
93 | return true; | |
94 | } | |
95 | ||
96 | static ParseFen(fen) { | |
97 | const fenParts = fen.split(" "); | |
98 | return Object.assign( | |
99 | ChessRules.ParseFen(fen), | |
100 | { reserve: fenParts[3] } | |
101 | ); | |
102 | } | |
103 | ||
104 | static get size() { | |
105 | return { x: 9, y: 9 }; | |
106 | } | |
107 | ||
108 | static GenRandInitFen(randomness) { | |
109 | return "9/9/9/9/9/9/9/9/9 w 0 12,12"; | |
110 | } | |
111 | ||
112 | getFen() { | |
113 | return super.getFen() + " " + this.getReserveFen(); | |
114 | } | |
115 | ||
116 | getFenForRepeat() { | |
117 | return super.getFenForRepeat() + "_" + this.getReserveFen(); | |
118 | } | |
119 | ||
120 | getReserveFen() { | |
121 | return ( | |
122 | (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," + | |
123 | (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN]) | |
124 | ); | |
125 | } | |
126 | ||
127 | getReservePpath(index, color) { | |
128 | return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@'; | |
129 | } | |
130 | ||
131 | static get RESERVE_PIECES() { | |
132 | return [V.PAWN]; //only array length matters | |
133 | } | |
134 | ||
135 | setOtherVariables(fen) { | |
136 | const reserve = | |
137 | V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10)); | |
138 | this.reserve = { | |
139 | w: { [V.PAWN]: reserve[0] }, | |
140 | b: { [V.PAWN]: reserve[1] } | |
141 | }; | |
142 | // Local stack of captures during a turn (squares + directions) | |
143 | this.captures = [ [] ]; | |
144 | } | |
145 | ||
146 | atLeastOneCaptureFrom([x, y], color) { | |
147 | for (let s of V.steps[V.BISHOP]) { | |
148 | const [i, j] = [x + s[0], y + s[1]]; | |
149 | if ( | |
150 | V.OnBoard(i + s[0], j + s[1]) && | |
151 | this.board[i][j] != V.EMPTY && | |
152 | this.getColor(i, j) != color && | |
153 | this.board[i + s[0]][j + s[1]] == V.EMPTY | |
154 | ) { | |
155 | return true; | |
156 | } | |
157 | } | |
158 | return false; | |
159 | } | |
160 | ||
161 | atLeastOneCapture(color) { | |
162 | const L0 = this.captures.length; | |
163 | const captures = this.captures[L0 - 1]; | |
164 | const L = captures.length; | |
165 | if (L > 0) return this.atLeastOneCaptureFrom(captures[L-1].square, color); | |
166 | for (let i = 0; i < V.size.x; i++) { | |
167 | for (let j=0; j< V.size.y; j++) { | |
168 | if ( | |
169 | this.board[i][j] != V.EMPTY && | |
170 | this.getColor(i, j) == color && | |
171 | this.atLeastOneCaptureFrom([i, j], color) | |
172 | ) { | |
173 | return true; | |
174 | } | |
175 | } | |
176 | } | |
177 | return false; | |
178 | } | |
179 | ||
180 | maxLengthIndices(caps) { | |
181 | let maxLength = 0; | |
182 | let res = []; | |
183 | for (let i = 0; i < caps.length; i++) { | |
184 | if (caps[i].length > maxLength) { | |
185 | res = [i]; | |
186 | maxLength = caps[i].length; | |
187 | } | |
188 | else if (caps[i].length == maxLength) res.push(i); | |
189 | } | |
190 | return res; | |
191 | }; | |
192 | ||
193 | getLongestCapturesFrom([x, y], color, locSteps) { | |
194 | // | |
195 | // TODO: debug here, from | |
196 | // 9/9/2a@1a@4/5A@3/9/3aa1A@3/9/9/8A@ w 10 8,9 | |
197 | // White to move, double capture. | |
198 | // | |
199 | let res = []; | |
200 | const L = locSteps.length; | |
201 | const lastStep = (L > 0 ? locSteps[L-1] : null); | |
202 | for (let s of V.steps[V.BISHOP]) { | |
203 | if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue; | |
204 | const [i, j] = [x + s[0], y + s[1]]; | |
205 | if ( | |
206 | V.OnBoard(i + s[0], j + s[1]) && | |
207 | this.board[i + s[0]][j + s[1]] == V.EMPTY && | |
208 | this.board[i][j] != V.EMPTY && | |
209 | this.getColor(i, j) != color | |
210 | ) { | |
211 | const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]); | |
212 | locSteps.push(s); | |
213 | V.PlayOnBoard(this.board, move); | |
214 | const sRes = this.getLongestCapturesFrom( | |
215 | [i + s[0], j + s[1]], color, locSteps); | |
216 | res.push({ | |
217 | step: s, | |
218 | length: 1 + (sRes.length == 0 ? 0 : sRes[0].length) | |
219 | }); | |
220 | locSteps.pop(); | |
221 | V.UndoOnBoard(this.board, move); | |
222 | } | |
223 | } | |
224 | return this.maxLengthIndices(res).map(i => res[i]); | |
225 | } | |
226 | ||
227 | getAllLongestCaptures(color) { | |
228 | const L0 = this.captures.length; | |
229 | const captures = this.captures[L0 - 1]; | |
230 | const L = captures.length; | |
231 | if (L > 0) { | |
232 | let locSteps = []; | |
233 | const caps = Object.assign( | |
234 | { square: captures[L-1].square }, | |
235 | this.getLongestCapturesFrom(captures[L-1].square, color, locSteps) | |
236 | ); | |
237 | return this.maxLengthIndices(caps).map(i => caps[i]); | |
238 | } | |
239 | let caps = []; | |
240 | for (let i = 0; i < V.size.x; i++) { | |
241 | for (let j=0; j < V.size.y; j++) { | |
242 | if ( | |
243 | this.board[i][j] != V.EMPTY && | |
244 | this.getColor(i, j) == color | |
245 | ) { | |
246 | let locSteps = []; | |
247 | let res = this.getLongestCapturesFrom([i, j], color, locSteps); | |
248 | Array.prototype.push.apply( | |
249 | caps, | |
250 | res.map(r => Object.assign({ square: [i, j] }, r)) | |
251 | ); | |
252 | } | |
253 | } | |
254 | } | |
255 | ||
256 | console.log(caps); | |
257 | ||
258 | return this.maxLengthIndices(caps).map(i => caps[i]); | |
259 | } | |
260 | ||
261 | getBasicMove([x1, y1], [x2, y2], capt) { | |
262 | const cp1 = this.board[x1][y1]; | |
263 | if (!capt) { | |
264 | return new Move({ | |
265 | appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ], | |
266 | vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ] | |
267 | }); | |
268 | } | |
269 | // Compute resulting types based on jumped + jumping pieces | |
270 | const cpCapt = this.board[capt[0]][capt[1]]; | |
271 | const newAtCapt = cpCapt.charCodeAt(0) - 1; | |
272 | const newAtDest = | |
273 | cp1[1] == '@' | |
274 | ? (cp1.charCodeAt(0) < 97 ? 65 : 97) | |
275 | : (cp1.charCodeAt(1) + 1); | |
276 | const color = this.turn; | |
277 | let mv = new Move({ | |
278 | appear: [ | |
279 | new PiPo({ | |
280 | x: x2, | |
281 | y: y2, | |
282 | c: cp1[0], | |
283 | p: String.fromCharCode(newAtDest) | |
284 | }) | |
285 | ], | |
286 | vanish: [ | |
287 | new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }), | |
288 | new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] }) | |
289 | ] | |
290 | }); | |
291 | if ([64, 96].includes(newAtCapt)) { | |
292 | // Enemy units vanish from capturing square | |
293 | if (cpCapt.charAt(1) != '@') { | |
294 | // Out units remain: | |
295 | mv.appear.push( | |
296 | new PiPo({ | |
297 | x: capt[0], | |
298 | y: capt[1], | |
299 | c: cpCapt[0], | |
300 | p: '@' | |
301 | }) | |
302 | ); | |
303 | } | |
304 | } | |
305 | else { | |
306 | mv.appear.push( | |
307 | new PiPo({ | |
308 | x: capt[0], | |
309 | y: capt[1], | |
310 | c: String.fromCharCode(newAtCapt), | |
311 | p: cpCapt[1] | |
312 | }) | |
313 | ); | |
314 | } | |
315 | return mv; | |
316 | } | |
317 | ||
318 | getReserveMoves(x) { | |
319 | const color = this.turn; | |
320 | if (!this.reserve[color] || this.atLeastOneCapture(color)) return []; | |
321 | let moves = []; | |
322 | const shadowPiece = | |
323 | this.reserve[V.GetOppCol(color)] == null | |
324 | ? this.reserve[color][V.PAWN] - 1 | |
325 | : 0; | |
326 | const appearColor = String.fromCharCode( | |
327 | (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece); | |
328 | const addMove = ([i, j]) => { | |
329 | moves.push( | |
330 | new Move({ | |
331 | appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ], | |
332 | vanish: [], | |
333 | start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 } | |
334 | }) | |
335 | ); | |
336 | }; | |
337 | const oppCol = V.GetOppCol(color); | |
338 | const opponentCanCapture = this.atLeastOneCapture(oppCol); | |
339 | for (let i = 0; i < V.size.x; i++) { | |
340 | for (let j = i % 2; j < V.size.y; j += 2) { | |
341 | if ( | |
342 | this.board[i][j] == V.EMPTY && | |
343 | // prevent playing on central square at move 1: | |
344 | (this.movesCount >= 1 || i != 4 || j != 4) | |
345 | ) { | |
346 | if (opponentCanCapture) addMove([i, j]); | |
347 | else { | |
348 | let canAddMove = true; | |
349 | for (let s of V.steps[V.BISHOP]) { | |
350 | if ( | |
351 | V.OnBoard(i + s[0], j + s[1]) && | |
352 | V.OnBoard(i - s[0], j - s[1]) && | |
353 | this.board[i + s[0]][j + s[1]] != V.EMPTY && | |
354 | this.board[i - s[0]][j - s[1]] == V.EMPTY && | |
355 | this.getColor(i + s[0], j + s[1]) == oppCol | |
356 | ) { | |
357 | canAddMove = false; | |
358 | break; | |
359 | } | |
360 | } | |
361 | if (canAddMove) addMove([i, j]); | |
362 | } | |
363 | } | |
364 | } | |
365 | } | |
366 | return moves; | |
367 | } | |
368 | ||
369 | getPotentialMovesFrom([x, y], longestCaptures) { | |
370 | if (x >= V.size.x) { | |
371 | if (longestCaptures.length == 0) return this.getReserveMoves(x); | |
372 | return []; | |
373 | } | |
374 | const color = this.turn; | |
375 | const L0 = this.captures.length; | |
376 | const captures = this.captures[L0 - 1]; | |
377 | const L = captures.length; | |
378 | let moves = []; | |
379 | if (longestCaptures.length > 0) { | |
380 | if ( | |
381 | L > 0 && | |
382 | (x != captures[L-1].square[0] || y != captures[L-1].square[1]) | |
383 | ) { | |
384 | return []; | |
385 | } | |
386 | longestCaptures.forEach(lc => { | |
387 | if (lc.square[0] == x && lc.square[1] == y) { | |
388 | const s = lc.step; | |
389 | const [i, j] = [x + s[0], y + s[1]]; | |
390 | moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j])); | |
391 | } | |
392 | }); | |
393 | return moves; | |
394 | } | |
395 | // Just search simple moves: | |
396 | for (let s of V.steps[V.BISHOP]) { | |
397 | const [i, j] = [x + s[0], y + s[1]]; | |
398 | if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) | |
399 | moves.push(this.getBasicMove([x, y], [i, j])); | |
400 | } | |
401 | return moves; | |
402 | } | |
403 | ||
404 | getAllValidMoves() { | |
405 | const color = this.turn; | |
406 | const longestCaptures = this.getAllLongestCaptures(color); | |
407 | let potentialMoves = []; | |
408 | for (let i = 0; i < V.size.x; i++) { | |
409 | for (let j = 0; j < V.size.y; j++) { | |
410 | if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { | |
411 | Array.prototype.push.apply( | |
412 | potentialMoves, | |
413 | this.getPotentialMovesFrom([i, j], longestCaptures) | |
414 | ); | |
415 | } | |
416 | } | |
417 | } | |
418 | // Add reserve moves | |
419 | potentialMoves = potentialMoves.concat( | |
420 | this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1)) | |
421 | ); | |
422 | return potentialMoves; | |
423 | } | |
424 | ||
425 | getPossibleMovesFrom([x, y]) { | |
426 | const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y)); | |
427 | return this.getPotentialMovesFrom([x, y], longestCaptures); | |
428 | } | |
429 | ||
430 | filterValid(moves) { | |
431 | return moves; | |
432 | } | |
433 | ||
434 | getCheckSquares() { | |
435 | return []; | |
436 | } | |
437 | ||
438 | play(move) { | |
439 | const color = this.turn; | |
440 | move.turn = color; //for undo | |
441 | V.PlayOnBoard(this.board, move); | |
442 | if (move.vanish.length == 2) { | |
443 | const L0 = this.captures.length; | |
444 | let captures = this.captures[L0 - 1]; | |
445 | captures.push({ | |
446 | square: [move.start.x, move.start.y], | |
447 | step: [move.end.x - move.start.x, move.end.y - move.start.y] | |
448 | }); | |
449 | if (this.atLeastOneCapture()) | |
450 | // There could be other captures (optional) | |
451 | move.notTheEnd = true; | |
452 | } | |
453 | else if (move.vanish == 0) { | |
454 | if (--this.reserve[color][V.PAWN] == 0) this.reserve[color] = null; | |
455 | } | |
456 | if (!move.notTheEnd) { | |
457 | this.turn = V.GetOppCol(color); | |
458 | this.movesCount++; | |
459 | this.captures.push([]); | |
460 | } | |
461 | } | |
462 | ||
463 | undo(move) { | |
464 | V.UndoOnBoard(this.board, move); | |
465 | if (!move.notTheEnd) { | |
466 | this.turn = move.turn; | |
467 | this.movesCount--; | |
468 | this.captures.pop(); | |
469 | } | |
470 | if (move.vanish.length == 0) { | |
471 | const color = (move.appear[0].c == 'A' ? 'w' : 'b'); | |
472 | if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 1 }; | |
473 | else this.reserve[color][V.PAWN]++; | |
474 | } | |
475 | else if (move.vanish.length == 2) { | |
476 | const L0 = this.captures.length; | |
477 | let captures = this.captures[L0 - 1]; | |
478 | captures.pop(); | |
479 | } | |
480 | } | |
481 | ||
482 | atLeastOneMove() { | |
483 | if (this.atLeastOneCapture()) return true; | |
484 | const color = this.turn; | |
485 | for (let i = 0; i < V.size.x; i++) { | |
486 | for (let j = 0; j < V.size.y; j++) { | |
487 | if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { | |
488 | const moves = this.getPotentialMovesFrom([i, j], []); | |
489 | if (moves.length > 0) return true; | |
490 | } | |
491 | } | |
492 | } | |
493 | const reserveMoves = | |
494 | this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1)); | |
495 | return (reserveMoves.length > 0); | |
496 | } | |
497 | ||
498 | getCurrentScore() { | |
499 | const color = this.turn; | |
500 | // If no pieces on board + reserve, I lose | |
501 | if ( | |
502 | !this.reserve[color] && | |
503 | this.board.every(b => { | |
504 | return b.every(cell => { | |
505 | return (cell == "" || cell[0] != color); | |
506 | }); | |
507 | }) | |
508 | ) { | |
509 | return (color == 'w' ? "0-1" : "1-0"); | |
510 | } | |
511 | if (!this.atLeastOneMove()) return "1/2"; | |
512 | return "*"; | |
513 | } | |
514 | ||
515 | getComputerMove() { | |
516 | // Random mover for now (TODO) | |
517 | const color = this.turn; | |
518 | let mvArray = []; | |
519 | let mv = null; | |
520 | while (this.turn == color) { | |
521 | const moves = this.getAllValidMoves(); | |
522 | mv = moves[randInt(moves.length)]; | |
523 | mvArray.push(mv); | |
524 | this.play(mv); | |
525 | } | |
526 | for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); | |
527 | return (mvArray.length > 1 ? mvArray : mvArray[0]); | |
528 | } | |
529 | ||
530 | getNotation(move) { | |
531 | if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end); | |
532 | return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end); | |
533 | } | |
d2af3400 BA |
534 | |
535 | }; |