Commit | Line | Data |
---|---|---|
1e8a8386 | 1 | import { ChessRules, Move, PiPo } from "@/base_rules"; |
1269441e BA |
2 | |
3 | export class SynochessRules extends ChessRules { | |
4 | ||
4313762d BA |
5 | static get Options() { |
6 | return { | |
7 | check: [ | |
8 | { | |
9 | label: "Random", | |
10 | defaut: false, | |
11 | variable: "random" | |
12 | } | |
13 | ] | |
14 | }; | |
15 | } | |
16 | ||
1e8a8386 BA |
17 | static get LoseOnRepetition() { |
18 | return true; | |
19 | } | |
20 | ||
21 | static IsGoodFlags(flags) { | |
22 | // Only white can castle | |
23 | return !!flags.match(/^[a-z]{2,2}$/); | |
24 | } | |
25 | ||
26 | static IsGoodFen(fen) { | |
27 | if (!ChessRules.IsGoodFen(fen)) return false; | |
28 | const fenParsed = V.ParseFen(fen); | |
29 | // 5) Check reserves | |
30 | if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-2]$/)) | |
31 | return false; | |
32 | return true; | |
33 | } | |
34 | ||
35 | static ParseFen(fen) { | |
36 | const fenParts = fen.split(" "); | |
37 | return Object.assign( | |
38 | ChessRules.ParseFen(fen), | |
39 | { reserve: fenParts[5] } | |
40 | ); | |
41 | } | |
42 | ||
4313762d BA |
43 | static GenRandInitFen(options) { |
44 | if (!options.random) | |
1e8a8386 BA |
45 | return "rneakenr/8/1c4c1/1ss2ss1/8/8/PPPPPPPP/RNBQKBNR w 0 ah - 2"; |
46 | ||
47 | // Mapping kingdom --> dynasty: | |
48 | const piecesMap = { | |
49 | 'r': 'r', | |
50 | 'n': 'n', | |
51 | 'b': 'e', | |
52 | 'q': 'a', | |
53 | 'k': 'k' | |
54 | }; | |
55 | ||
56 | // Always symmetric (randomness = 1), because open files. | |
4313762d | 57 | const baseFen = ChessRules.GenRandInitFen({ randomness: 1 }); |
1e8a8386 BA |
58 | return ( |
59 | baseFen.substr(0, 8).split("").map(p => piecesMap[p]).join("") + | |
60 | "/8/1c4c1/1ss2ss1/" + baseFen.substr(22, 28) + " - 2" | |
61 | ); | |
62 | } | |
63 | ||
64 | getReserveFen() { | |
65 | return (!!this.reserve ? this.reserve["b"][V.SOLDIER] : 0); | |
66 | } | |
67 | ||
68 | getFen() { | |
69 | return super.getFen() + " " + this.getReserveFen(); | |
70 | } | |
71 | ||
72 | getFenForRepeat() { | |
73 | return super.getFenForRepeat() + "_" + this.getReserveFen(); | |
74 | } | |
75 | ||
76 | setOtherVariables(fen) { | |
77 | super.setOtherVariables(fen); | |
78 | // Also init reserve (used by the interface to show landable soldiers) | |
79 | const reserve = parseInt(V.ParseFen(fen).reserve, 10); | |
80 | if (reserve > 0) this.reserve = { 'b': { [V.SOLDIER]: reserve } }; | |
81 | } | |
82 | ||
83 | getColor(i, j) { | |
84 | if (i >= V.size.x) return 'b'; | |
85 | return this.board[i][j].charAt(0); | |
86 | } | |
87 | ||
88 | getPiece(i, j) { | |
89 | if (i >= V.size.x) return V.SOLDIER; | |
90 | return this.board[i][j].charAt(1); | |
91 | } | |
92 | ||
93 | getReservePpath(index, color) { | |
94 | // Only one piece type: soldier | |
95 | return "Synochess/" + color + V.SOLDIER; | |
96 | } | |
97 | ||
98 | static get RESERVE_PIECES() { | |
99 | return [V.SOLDIER]; | |
100 | } | |
101 | ||
102 | getReserveMoves(x) { | |
103 | const color = this.turn; | |
104 | if (!this.reserve || this.reserve[color][V.SOLDIER] == 0) return []; | |
105 | let moves = []; | |
106 | for (let i = 0; i < V.size.y; i++) { | |
107 | if (this.board[3][i] == V.EMPTY) { | |
108 | let mv = new Move({ | |
109 | appear: [ | |
110 | new PiPo({ | |
111 | x: 3, | |
112 | y: i, | |
113 | c: color, | |
114 | p: V.SOLDIER | |
115 | }) | |
116 | ], | |
117 | vanish: [], | |
118 | start: { x: x, y: 0 }, //a bit artificial... | |
119 | end: { x: 3, y: i } | |
120 | }); | |
121 | moves.push(mv); | |
122 | } | |
123 | } | |
124 | return moves; | |
125 | } | |
126 | ||
127 | getPpath(b) { | |
128 | return (ChessRules.PIECES.includes(b[1]) ? "" : "Synochess/") + b; | |
129 | } | |
130 | ||
131 | getFlagsFen() { | |
132 | return this.castleFlags['w'].map(V.CoordToColumn).join(""); | |
133 | } | |
134 | ||
135 | setFlags(fenflags) { | |
136 | this.castleFlags = { 'w': [-1, -1] }; | |
137 | for (let i = 0; i < 2; i++) | |
138 | this.castleFlags['w'][i] = V.ColumnToCoord(fenflags.charAt(i)); | |
139 | } | |
140 | ||
141 | static get ELEPHANT() { | |
142 | return "e"; | |
143 | } | |
144 | ||
145 | static get CANNON() { | |
146 | return "c"; | |
147 | } | |
148 | ||
149 | static get SOLDIER() { | |
150 | return "s"; | |
151 | } | |
152 | ||
153 | static get ADVISOR() { | |
154 | return "a"; | |
155 | } | |
156 | ||
157 | static get PIECES() { | |
158 | return ( | |
159 | ChessRules.PIECES.concat([V.ELEPHANT, V.ADVISOR, V.SOLDIER, V.CANNON]) | |
160 | ); | |
161 | } | |
162 | ||
163 | static get steps() { | |
164 | return Object.assign( | |
165 | {}, | |
166 | ChessRules.steps, | |
167 | { | |
168 | e: [ | |
169 | [-1, -1], | |
170 | [-1, 1], | |
171 | [1, -1], | |
172 | [1, 1], | |
173 | [-2, -2], | |
174 | [-2, 2], | |
175 | [2, -2], | |
176 | [2, 2] | |
177 | ] | |
178 | } | |
179 | ); | |
180 | } | |
181 | ||
182 | getPotentialMovesFrom(sq) { | |
183 | if (sq[0] >= V.size.x) | |
184 | // Reserves, outside of board: x == sizeX(+1) | |
185 | return this.getReserveMoves(sq[0]); | |
186 | let moves = []; | |
187 | const piece = this.getPiece(sq[0], sq[1]); | |
188 | switch (piece) { | |
189 | case V.CANNON: | |
190 | moves = this.getPotentialCannonMoves(sq); | |
191 | break; | |
192 | case V.ELEPHANT: | |
193 | moves = this.getPotentialElephantMoves(sq); | |
194 | break; | |
195 | case V.ADVISOR: | |
196 | moves = this.getPotentialAdvisorMoves(sq); | |
197 | break; | |
198 | case V.SOLDIER: | |
199 | moves = this.getPotentialSoldierMoves(sq); | |
200 | break; | |
201 | default: | |
202 | moves = super.getPotentialMovesFrom(sq); | |
203 | } | |
204 | if ( | |
205 | piece != V.KING && | |
206 | this.kingPos['w'][0] != this.kingPos['b'][0] && | |
207 | this.kingPos['w'][1] != this.kingPos['b'][1] | |
208 | ) { | |
209 | return moves; | |
210 | } | |
211 | // TODO: from here, copy/paste from EmpireChess | |
212 | // TODO: factor two next "if" into one (rank/column...) | |
213 | if (this.kingPos['w'][1] == this.kingPos['b'][1]) { | |
214 | const colKing = this.kingPos['w'][1]; | |
215 | let intercept = 0; //count intercepting pieces | |
216 | let [kingPos1, kingPos2] = [this.kingPos['w'][0], this.kingPos['b'][0]]; | |
217 | if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; | |
218 | for (let i = kingPos1 + 1; i < kingPos2; i++) { | |
219 | if (this.board[i][colKing] != V.EMPTY) intercept++; | |
220 | } | |
221 | if (intercept >= 2) return moves; | |
222 | // intercept == 1 (0 is impossible): | |
223 | // Any move not removing intercept is OK | |
224 | return moves.filter(m => { | |
225 | return ( | |
226 | // From another column? | |
227 | m.start.y != colKing || | |
228 | // From behind a king? (including kings themselves!) | |
229 | m.start.x <= kingPos1 || | |
230 | m.start.x >= kingPos2 || | |
231 | // Intercept piece moving: must remain in-between | |
232 | ( | |
233 | m.end.y == colKing && | |
234 | m.end.x > kingPos1 && | |
235 | m.end.x < kingPos2 | |
236 | ) | |
237 | ); | |
238 | }); | |
239 | } | |
240 | if (this.kingPos['w'][0] == this.kingPos['b'][0]) { | |
241 | const rowKing = this.kingPos['w'][0]; | |
242 | let intercept = 0; //count intercepting pieces | |
243 | let [kingPos1, kingPos2] = [this.kingPos['w'][1], this.kingPos['b'][1]]; | |
244 | if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; | |
245 | for (let i = kingPos1 + 1; i < kingPos2; i++) { | |
246 | if (this.board[rowKing][i] != V.EMPTY) intercept++; | |
247 | } | |
248 | if (intercept >= 2) return moves; | |
249 | // intercept == 1 (0 is impossible): | |
250 | // Any move not removing intercept is OK | |
251 | return moves.filter(m => { | |
252 | return ( | |
253 | // From another row? | |
254 | m.start.x != rowKing || | |
255 | // From "behind" a king? (including kings themselves!) | |
256 | m.start.y <= kingPos1 || | |
257 | m.start.y >= kingPos2 || | |
258 | // Intercept piece moving: must remain in-between | |
259 | ( | |
260 | m.end.x == rowKing && | |
261 | m.end.y > kingPos1 && | |
262 | m.end.y < kingPos2 | |
263 | ) | |
264 | ); | |
265 | }); | |
266 | } | |
267 | // piece == king: check only if move.end.y == enemy king column, | |
268 | // or if move.end.x == enemy king rank. | |
269 | const color = this.getColor(sq[0], sq[1]); | |
270 | const oppCol = V.GetOppCol(color); | |
1e8a8386 BA |
271 | return moves.filter(m => { |
272 | if ( | |
273 | m.end.y != this.kingPos[oppCol][1] && | |
274 | m.end.x != this.kingPos[oppCol][0] | |
275 | ) { | |
276 | return true; | |
277 | } | |
6a9fc539 BA |
278 | // check == -1 if (row, or col) unchecked, 1 if checked and occupied, |
279 | // 0 if checked and clear | |
280 | let check = [-1, -1]; | |
1e8a8386 BA |
281 | // TODO: factor two next "if"... |
282 | if (m.end.x == this.kingPos[oppCol][0]) { | |
283 | if (check[0] < 0) { | |
284 | // Do the check: | |
285 | check[0] = 0; | |
6a9fc539 | 286 | let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]]; |
1e8a8386 BA |
287 | if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; |
288 | for (let i = kingPos1 + 1; i < kingPos2; i++) { | |
289 | if (this.board[m.end.x][i] != V.EMPTY) { | |
290 | check[0]++; | |
291 | break; | |
292 | } | |
293 | } | |
294 | return check[0] == 1; | |
295 | } | |
296 | // Check already done: | |
297 | return check[0] == 1; | |
298 | } | |
299 | //if (m.end.y == this.kingPos[oppCol][1]) //true... | |
300 | if (check[1] < 0) { | |
301 | // Do the check: | |
302 | check[1] = 0; | |
6a9fc539 | 303 | let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]]; |
1e8a8386 BA |
304 | if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; |
305 | for (let i = kingPos1 + 1; i < kingPos2; i++) { | |
306 | if (this.board[i][m.end.y] != V.EMPTY) { | |
307 | check[1]++; | |
308 | break; | |
309 | } | |
310 | } | |
311 | return check[1] == 1; | |
312 | } | |
313 | // Check already done: | |
314 | return check[1] == 1; | |
315 | }); | |
316 | } | |
317 | ||
318 | getPotentialAdvisorMoves(sq) { | |
319 | return super.getSlideNJumpMoves( | |
4313762d | 320 | sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); |
1e8a8386 BA |
321 | } |
322 | ||
323 | getPotentialKingMoves([x, y]) { | |
324 | if (this.getColor(x, y) == 'w') return super.getPotentialKingMoves([x, y]); | |
325 | // Dynasty doesn't castle: | |
326 | return super.getSlideNJumpMoves( | |
4313762d | 327 | [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); |
1e8a8386 BA |
328 | } |
329 | ||
330 | getPotentialSoldierMoves([x, y]) { | |
331 | const c = this.getColor(x, y); | |
332 | const shiftX = (c == 'w' ? -1 : 1); | |
333 | const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9); | |
334 | let steps = []; | |
335 | if (!lastRank) steps.push([shiftX, 0]); | |
336 | if (y > 0) steps.push([0, -1]); | |
337 | if (y < 9) steps.push([0, 1]); | |
4313762d | 338 | return super.getSlideNJumpMoves([x, y], steps, 1); |
1e8a8386 BA |
339 | } |
340 | ||
341 | getPotentialElephantMoves([x, y]) { | |
4313762d | 342 | return this.getSlideNJumpMoves([x, y], V.steps[V.ELEPHANT], 1); |
1e8a8386 BA |
343 | } |
344 | ||
345 | // NOTE: (mostly) duplicated from Shako (TODO?) | |
346 | getPotentialCannonMoves([x, y]) { | |
347 | const oppCol = V.GetOppCol(this.turn); | |
348 | let moves = []; | |
349 | // Look in every direction until an obstacle (to jump) is met | |
350 | for (const step of V.steps[V.ROOK]) { | |
351 | let i = x + step[0]; | |
352 | let j = y + step[1]; | |
353 | while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
1e8a8386 BA |
354 | i += step[0]; |
355 | j += step[1]; | |
356 | } | |
357 | // Then, search for an enemy (if jumped piece isn't a cannon) | |
358 | if (V.OnBoard(i, j) && this.getPiece(i, j) != V.CANNON) { | |
359 | i += step[0]; | |
360 | j += step[1]; | |
361 | while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
dbc79ee6 | 362 | moves.push(this.getBasicMove([x, y], [i, j])); |
1e8a8386 BA |
363 | i += step[0]; |
364 | j += step[1]; | |
365 | } | |
366 | if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol) | |
367 | moves.push(this.getBasicMove([x, y], [i, j])); | |
368 | } | |
369 | } | |
370 | return moves; | |
371 | } | |
372 | ||
373 | isAttacked(sq, color) { | |
374 | return ( | |
375 | super.isAttackedByRook(sq, color) || | |
376 | super.isAttackedByKnight(sq, color) || | |
377 | super.isAttackedByKing(sq, color) || | |
378 | ( | |
379 | color == 'w' && | |
380 | ( | |
381 | super.isAttackedByPawn(sq, color) || | |
382 | super.isAttackedByBishop(sq, color) || | |
383 | super.isAttackedByQueen(sq, color) | |
384 | ) | |
385 | ) || | |
386 | ( | |
387 | color == 'b' && | |
388 | ( | |
389 | this.isAttackedByCannon(sq, color) || | |
390 | this.isAttackedBySoldier(sq, color) || | |
391 | this.isAttackedByAdvisor(sq, color) || | |
392 | this.isAttackedByElephant(sq, color) | |
393 | ) | |
394 | ) | |
395 | ); | |
396 | } | |
397 | ||
398 | // NOTE: (mostly) duplicated from Shako (TODO?) | |
399 | isAttackedByCannon([x, y], color) { | |
400 | // Reversed process: is there an obstacle in line, | |
401 | // and a cannon next in the same line? | |
402 | for (const step of V.steps[V.ROOK]) { | |
403 | let [i, j] = [x+step[0], y+step[1]]; | |
404 | while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
405 | i += step[0]; | |
406 | j += step[1]; | |
407 | } | |
408 | if (V.OnBoard(i, j) && this.getPiece(i, j) != V.CANNON) { | |
409 | // Keep looking in this direction | |
410 | i += step[0]; | |
411 | j += step[1]; | |
412 | while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
413 | i += step[0]; | |
414 | j += step[1]; | |
415 | } | |
416 | if ( | |
417 | V.OnBoard(i, j) && | |
418 | this.getPiece(i, j) == V.CANNON && | |
419 | this.getColor(i, j) == color | |
420 | ) { | |
421 | return true; | |
422 | } | |
423 | } | |
424 | } | |
425 | return false; | |
426 | } | |
427 | ||
428 | isAttackedByAdvisor(sq, color) { | |
4313762d BA |
429 | return super.isAttackedBySlideNJump( |
430 | sq, color, V.ADVISOR, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); | |
1e8a8386 BA |
431 | } |
432 | ||
433 | isAttackedByElephant(sq, color) { | |
4313762d BA |
434 | return this.isAttackedBySlideNJump( |
435 | sq, color, V.ELEPHANT, V.steps[V.ELEPHANT], 1); | |
1e8a8386 BA |
436 | } |
437 | ||
438 | isAttackedBySoldier([x, y], color) { | |
439 | const shiftX = (color == 'w' ? 1 : -1); //shift from king | |
440 | return super.isAttackedBySlideNJump( | |
4313762d | 441 | [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1); |
1e8a8386 BA |
442 | } |
443 | ||
444 | getAllValidMoves() { | |
445 | let moves = super.getAllPotentialMoves(); | |
446 | const color = this.turn; | |
447 | if (!!this.reserve && color == 'b') | |
448 | moves = moves.concat(this.getReserveMoves(V.size.x + 1)); | |
449 | return this.filterValid(moves); | |
450 | } | |
451 | ||
452 | atLeastOneMove() { | |
453 | if (!super.atLeastOneMove()) { | |
454 | if (!!this.reserve && this.turn == 'b') { | |
455 | let moves = this.filterValid(this.getReserveMoves(V.size.x + 1)); | |
456 | if (moves.length > 0) return true; | |
457 | } | |
458 | return false; | |
459 | } | |
460 | return true; | |
461 | } | |
462 | ||
463 | getCurrentScore() { | |
464 | // Turn has changed: | |
465 | const color = V.GetOppCol(this.turn); | |
466 | const lastRank = (color == 'w' ? 0 : 7); | |
467 | if (this.kingPos[color][0] == lastRank) | |
468 | // The opposing edge is reached! | |
469 | return color == "w" ? "1-0" : "0-1"; | |
470 | if (this.atLeastOneMove()) return "*"; | |
471 | // Game over | |
472 | const oppCol = this.turn; | |
473 | return (oppCol == "w" ? "0-1" : "1-0"); | |
474 | } | |
475 | ||
476 | updateCastleFlags(move, piece) { | |
477 | // Only white can castle: | |
a550511a | 478 | const firstRank = 7; |
1e8a8386 BA |
479 | if (piece == V.KING && move.appear[0].c == 'w') |
480 | this.castleFlags['w'] = [8, 8]; | |
481 | else if ( | |
482 | move.start.x == firstRank && | |
483 | this.castleFlags['w'].includes(move.start.y) | |
484 | ) { | |
485 | const flagIdx = (move.start.y == this.castleFlags['w'][0] ? 0 : 1); | |
486 | this.castleFlags['w'][flagIdx] = 8; | |
487 | } | |
488 | else if ( | |
489 | move.end.x == firstRank && | |
490 | this.castleFlags['w'].includes(move.end.y) | |
491 | ) { | |
492 | const flagIdx = (move.end.y == this.castleFlags['w'][0] ? 0 : 1); | |
493 | this.castleFlags['w'][flagIdx] = 8; | |
494 | } | |
495 | } | |
496 | ||
497 | postPlay(move) { | |
1e8a8386 | 498 | // After black move, turn == 'w': |
a550511a | 499 | if (!!this.reserve && this.turn == 'w' && move.vanish.length == 0) { |
1e8a8386 | 500 | if (--this.reserve['b'][V.SOLDIER] == 0) this.reserve = null; |
a550511a BA |
501 | } |
502 | else super.postPlay(move); | |
1e8a8386 BA |
503 | } |
504 | ||
505 | postUndo(move) { | |
1e8a8386 BA |
506 | if (this.turn == 'b' && move.vanish.length == 0) { |
507 | if (!this.reserve) this.reserve = { 'b': { [V.SOLDIER]: 1 } }; | |
508 | else this.reserve['b'][V.SOLDIER]++; | |
509 | } | |
a550511a | 510 | else super.postUndo(move); |
1e8a8386 BA |
511 | } |
512 | ||
513 | static get VALUES() { | |
514 | return Object.assign( | |
515 | { | |
516 | s: 2, | |
517 | a: 2.75, | |
518 | e: 2.75, | |
519 | c: 3 | |
520 | }, | |
521 | ChessRules.VALUES | |
522 | ); | |
523 | } | |
524 | ||
525 | static get SEARCH_DEPTH() { | |
526 | return 2; | |
527 | } | |
528 | ||
529 | evalPosition() { | |
530 | let evaluation = super.evalPosition(); | |
ded43c88 | 531 | if (this.turn == 'b' && !!this.reserve) |
1e8a8386 BA |
532 | // Add reserves: |
533 | evaluation += this.reserve['b'][V.SOLDIER] * V.VALUES[V.SOLDIER]; | |
534 | return evaluation; | |
535 | } | |
1269441e | 536 | |
a550511a BA |
537 | getNotation(move) { |
538 | if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end); | |
539 | return super.getNotation(move); | |
540 | } | |
541 | ||
1269441e | 542 | }; |