Fix Shogi.js
[vchess.git] / client / src / variants / Synochess.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2
3 export class SynochessRules extends ChessRules {
4
5 static get Options() {
6 return {
7 check: [
8 {
9 label: "Random",
10 defaut: false,
11 variable: "random"
12 }
13 ]
14 };
15 }
16
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
43 static GenRandInitFen(options) {
44 if (!options.random)
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.
57 const baseFen = ChessRules.GenRandInitFen({ randomness: 1 });
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);
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 }
278 // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
279 // 0 if checked and clear
280 let check = [-1, -1];
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;
286 let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]];
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;
303 let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]];
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(
320 sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
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(
327 [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
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]);
338 return super.getSlideNJumpMoves([x, y], steps, 1);
339 }
340
341 getPotentialElephantMoves([x, y]) {
342 return this.getSlideNJumpMoves([x, y], V.steps[V.ELEPHANT], 1);
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) {
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) {
362 moves.push(this.getBasicMove([x, y], [i, j]));
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) {
429 return super.isAttackedBySlideNJump(
430 sq, color, V.ADVISOR, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
431 }
432
433 isAttackedByElephant(sq, color) {
434 return this.isAttackedBySlideNJump(
435 sq, color, V.ELEPHANT, V.steps[V.ELEPHANT], 1);
436 }
437
438 isAttackedBySoldier([x, y], color) {
439 const shiftX = (color == 'w' ? 1 : -1); //shift from king
440 return super.isAttackedBySlideNJump(
441 [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1);
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:
478 const firstRank = 7;
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) {
498 // After black move, turn == 'w':
499 if (!!this.reserve && this.turn == 'w' && move.vanish.length == 0) {
500 if (--this.reserve['b'][V.SOLDIER] == 0) this.reserve = null;
501 }
502 else super.postPlay(move);
503 }
504
505 postUndo(move) {
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 }
510 else super.postUndo(move);
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();
531 if (this.turn == 'b' && !!this.reserve)
532 // Add reserves:
533 evaluation += this.reserve['b'][V.SOLDIER] * V.VALUES[V.SOLDIER];
534 return evaluation;
535 }
536
537 getNotation(move) {
538 if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
539 return super.getNotation(move);
540 }
541
542 };