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