Fixes around game observing. TODO: abort/resign/draw + corr
[vchess.git] / client / src / variants / Baroque.js
1 class BaroqueRules extends ChessRules
2 {
3 static get HasFlags() { return false; }
4
5 static get HasEnpassant() { return false; }
6
7 static getPpath(b)
8 {
9 if (b[1] == "m") //'m' for Immobilizer (I is too similar to 1)
10 return "Baroque/" + b;
11 return b; //usual piece
12 }
13
14 static get PIECES()
15 {
16 return ChessRules.PIECES.concat([V.IMMOBILIZER]);
17 }
18
19 // No castling, but checks, so keep track of kings
20 setOtherVariables(fen)
21 {
22 this.kingPos = {'w':[-1,-1], 'b':[-1,-1]};
23 const fenParts = fen.split(" ");
24 const position = fenParts[0].split("/");
25 for (let i=0; i<position.length; i++)
26 {
27 let k = 0;
28 for (let j=0; j<position[i].length; j++)
29 {
30 switch (position[i].charAt(j))
31 {
32 case 'k':
33 this.kingPos['b'] = [i,k];
34 break;
35 case 'K':
36 this.kingPos['w'] = [i,k];
37 break;
38 default:
39 let num = parseInt(position[i].charAt(j));
40 if (!isNaN(num))
41 k += (num-1);
42 }
43 k++;
44 }
45 }
46 }
47
48 static get IMMOBILIZER() { return 'm'; }
49 // Although other pieces keep their names here for coding simplicity,
50 // keep in mind that:
51 // - a "rook" is a coordinator, capturing by coordinating with the king
52 // - a "knight" is a long-leaper, capturing as in draughts
53 // - a "bishop" is a chameleon, capturing as its prey
54 // - a "queen" is a withdrawer, capturing by moving away from pieces
55
56 // Is piece on square (x,y) immobilized?
57 isImmobilized([x,y])
58 {
59 const piece = this.getPiece(x,y);
60 const color = this.getColor(x,y);
61 const oppCol = V.GetOppCol(color);
62 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
63 outerLoop:
64 for (let step of adjacentSteps)
65 {
66 const [i,j] = [x+step[0],y+step[1]];
67 if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY
68 && this.getColor(i,j) == oppCol)
69 {
70 const oppPiece = this.getPiece(i,j);
71 if (oppPiece == V.IMMOBILIZER)
72 {
73 // Moving is impossible only if this immobilizer is not neutralized
74 for (let step2 of adjacentSteps)
75 {
76 const [i2,j2] = [i+step2[0],j+step2[1]];
77 if (i2 == x && j2 == y)
78 continue; //skip initial piece!
79 if (V.OnBoard(i2,j2) && this.board[i2][j2] != V.EMPTY
80 && this.getColor(i2,j2) == color)
81 {
82 if ([V.BISHOP,V.IMMOBILIZER].includes(this.getPiece(i2,j2)))
83 return false;
84 }
85 }
86 return true; //immobilizer isn't neutralized
87 }
88 // Chameleons can't be immobilized twice, because there is only one immobilizer
89 if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER)
90 return true;
91 }
92 }
93 return false;
94 }
95
96 getPotentialMovesFrom([x,y])
97 {
98 // Pre-check: is thing on this square immobilized?
99 if (this.isImmobilized([x,y]))
100 return [];
101 switch (this.getPiece(x,y))
102 {
103 case V.IMMOBILIZER:
104 return this.getPotentialImmobilizerMoves([x,y]);
105 default:
106 return super.getPotentialMovesFrom([x,y]);
107 }
108 }
109
110 getSlideNJumpMoves([x,y], steps, oneStep)
111 {
112 const color = this.getColor(x,y);
113 const piece = this.getPiece(x,y);
114 let moves = [];
115 outerLoop:
116 for (let step of steps)
117 {
118 let i = x + step[0];
119 let j = y + step[1];
120 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
121 {
122 moves.push(this.getBasicMove([x,y], [i,j]));
123 if (oneStep !== undefined)
124 continue outerLoop;
125 i += step[0];
126 j += step[1];
127 }
128 // Only king can take on occupied square:
129 if (piece==V.KING && V.OnBoard(i,j) && this.canTake([x,y], [i,j]))
130 moves.push(this.getBasicMove([x,y], [i,j]));
131 }
132 return moves;
133 }
134
135 // Modify capturing moves among listed pawn moves
136 addPawnCaptures(moves, byChameleon)
137 {
138 const steps = V.steps[V.ROOK];
139 const color = this.turn;
140 const oppCol = V.GetOppCol(color);
141 moves.forEach(m => {
142 if (!!byChameleon && m.start.x!=m.end.x && m.start.y!=m.end.y)
143 return; //chameleon not moving as pawn
144 // Try capturing in every direction
145 for (let step of steps)
146 {
147 const sq2 = [m.end.x+2*step[0],m.end.y+2*step[1]];
148 if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] != V.EMPTY
149 && this.getColor(sq2[0],sq2[1]) == color)
150 {
151 // Potential capture
152 const sq1 = [m.end.x+step[0],m.end.y+step[1]];
153 if (this.board[sq1[0]][sq1[1]] != V.EMPTY
154 && this.getColor(sq1[0],sq1[1]) == oppCol)
155 {
156 const piece1 = this.getPiece(sq1[0],sq1[1]);
157 if (!byChameleon || piece1 == V.PAWN)
158 {
159 m.vanish.push(new PiPo({
160 x:sq1[0],
161 y:sq1[1],
162 c:oppCol,
163 p:piece1
164 }));
165 }
166 }
167 }
168 }
169 });
170 }
171
172 // "Pincer"
173 getPotentialPawnMoves([x,y])
174 {
175 let moves = super.getPotentialRookMoves([x,y]);
176 this.addPawnCaptures(moves);
177 return moves;
178 }
179
180 addRookCaptures(moves, byChameleon)
181 {
182 const color = this.turn;
183 const oppCol = V.GetOppCol(color);
184 const kp = this.kingPos[color];
185 moves.forEach(m => {
186 // Check piece-king rectangle (if any) corners for enemy pieces
187 if (m.end.x == kp[0] || m.end.y == kp[1])
188 return; //"flat rectangle"
189 const corner1 = [m.end.x, kp[1]];
190 const corner2 = [kp[0], m.end.y];
191 for (let [i,j] of [corner1,corner2])
192 {
193 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol)
194 {
195 const piece = this.getPiece(i,j);
196 if (!byChameleon || piece == V.ROOK)
197 {
198 m.vanish.push( new PiPo({
199 x:i,
200 y:j,
201 p:piece,
202 c:oppCol
203 }) );
204 }
205 }
206 }
207 });
208 }
209
210 // Coordinator
211 getPotentialRookMoves(sq)
212 {
213 let moves = super.getPotentialQueenMoves(sq);
214 this.addRookCaptures(moves);
215 return moves;
216 }
217
218 // Long-leaper
219 getKnightCaptures(startSquare, byChameleon)
220 {
221 // Look in every direction for captures
222 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
223 const color = this.turn;
224 const oppCol = V.GetOppCol(color);
225 let moves = [];
226 const [x,y] = [startSquare[0],startSquare[1]];
227 const piece = this.getPiece(x,y); //might be a chameleon!
228 outerLoop:
229 for (let step of steps)
230 {
231 let [i,j] = [x+step[0], y+step[1]];
232 while (V.OnBoard(i,j) && this.board[i][j]==V.EMPTY)
233 {
234 i += step[0];
235 j += step[1];
236 }
237 if (!V.OnBoard(i,j) || this.getColor(i,j)==color
238 || (!!byChameleon && this.getPiece(i,j)!=V.KNIGHT))
239 {
240 continue;
241 }
242 // last(thing), cur(thing) : stop if "cur" is our color, or beyond board limits,
243 // or if "last" isn't empty and cur neither. Otherwise, if cur is empty then
244 // add move until cur square; if cur is occupied then stop if !!byChameleon and
245 // the square not occupied by a leaper.
246 let last = [i,j];
247 let cur = [i+step[0],j+step[1]];
248 let vanished = [ new PiPo({x:x,y:y,c:color,p:piece}) ];
249 while (V.OnBoard(cur[0],cur[1]))
250 {
251 if (this.board[last[0]][last[1]] != V.EMPTY)
252 {
253 const oppPiece = this.getPiece(last[0],last[1]);
254 if (!!byChameleon && oppPiece != V.KNIGHT)
255 continue outerLoop;
256 // Something to eat:
257 vanished.push( new PiPo({x:last[0],y:last[1],c:oppCol,p:oppPiece}) );
258 }
259 if (this.board[cur[0]][cur[1]] != V.EMPTY)
260 {
261 if (this.getColor(cur[0],cur[1]) == color
262 || this.board[last[0]][last[1]] != V.EMPTY) //TODO: redundant test
263 {
264 continue outerLoop;
265 }
266 }
267 else
268 {
269 moves.push(new Move({
270 appear: [ new PiPo({x:cur[0],y:cur[1],c:color,p:piece}) ],
271 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
272 start: {x:x,y:y},
273 end: {x:cur[0],y:cur[1]}
274 }));
275 }
276 last = [last[0]+step[0],last[1]+step[1]];
277 cur = [cur[0]+step[0],cur[1]+step[1]];
278 }
279 }
280 return moves;
281 }
282
283 // Long-leaper
284 getPotentialKnightMoves(sq)
285 {
286 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
287 }
288
289 getPotentialBishopMoves([x,y])
290 {
291 let moves = super.getPotentialQueenMoves([x,y])
292 .concat(this.getKnightCaptures([x,y],"asChameleon"));
293 // No "king capture" because king cannot remain under check
294 this.addPawnCaptures(moves, "asChameleon");
295 this.addRookCaptures(moves, "asChameleon");
296 this.addQueenCaptures(moves, "asChameleon");
297 // Post-processing: merge similar moves, concatenating vanish arrays
298 let mergedMoves = {};
299 moves.forEach(m => {
300 const key = m.end.x + V.size.x * m.end.y;
301 if (!mergedMoves[key])
302 mergedMoves[key] = m;
303 else
304 {
305 for (let i=1; i<m.vanish.length; i++)
306 mergedMoves[key].vanish.push(m.vanish[i]);
307 }
308 });
309 // Finally return an array
310 moves = [];
311 Object.keys(mergedMoves).forEach(k => { moves.push(mergedMoves[k]); });
312 return moves;
313 }
314
315 // Withdrawer
316 addQueenCaptures(moves, byChameleon)
317 {
318 if (moves.length == 0)
319 return;
320 const [x,y] = [moves[0].start.x,moves[0].start.y];
321 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
322 let capturingDirections = [];
323 const color = this.turn;
324 const oppCol = V.GetOppCol(color);
325 adjacentSteps.forEach(step => {
326 const [i,j] = [x+step[0],y+step[1]];
327 if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol
328 && (!byChameleon || this.getPiece(i,j) == V.QUEEN))
329 {
330 capturingDirections.push(step);
331 }
332 });
333 moves.forEach(m => {
334 const step = [
335 m.end.x!=x ? (m.end.x-x)/Math.abs(m.end.x-x) : 0,
336 m.end.y!=y ? (m.end.y-y)/Math.abs(m.end.y-y) : 0
337 ];
338 // NOTE: includes() and even _.isEqual() functions fail...
339 // TODO: this test should be done only once per direction
340 if (capturingDirections.some(dir =>
341 { return (dir[0]==-step[0] && dir[1]==-step[1]); }))
342 {
343 const [i,j] = [x-step[0],y-step[1]];
344 m.vanish.push(new PiPo({
345 x:i,
346 y:j,
347 p:this.getPiece(i,j),
348 c:oppCol
349 }));
350 }
351 });
352 }
353
354 getPotentialQueenMoves(sq)
355 {
356 let moves = super.getPotentialQueenMoves(sq);
357 this.addQueenCaptures(moves);
358 return moves;
359 }
360
361 getPotentialImmobilizerMoves(sq)
362 {
363 // Immobilizer doesn't capture
364 return super.getPotentialQueenMoves(sq);
365 }
366
367 getPotentialKingMoves(sq)
368 {
369 return this.getSlideNJumpMoves(sq,
370 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
371 }
372
373 // isAttacked() is OK because the immobilizer doesn't take
374
375 isAttackedByPawn([x,y], colors)
376 {
377 // Square (x,y) must be surroundable by two enemy pieces,
378 // and one of them at least should be a pawn (moving).
379 const dirs = [ [1,0],[0,1] ];
380 const steps = V.steps[V.ROOK];
381 for (let dir of dirs)
382 {
383 const [i1,j1] = [x-dir[0],y-dir[1]]; //"before"
384 const [i2,j2] = [x+dir[0],y+dir[1]]; //"after"
385 if (V.OnBoard(i1,j1) && V.OnBoard(i2,j2))
386 {
387 if ((this.board[i1][j1]!=V.EMPTY && colors.includes(this.getColor(i1,j1))
388 && this.board[i2][j2]==V.EMPTY)
389 ||
390 (this.board[i2][j2]!=V.EMPTY && colors.includes(this.getColor(i2,j2))
391 && this.board[i1][j1]==V.EMPTY))
392 {
393 // Search a movable enemy pawn landing on the empty square
394 for (let step of steps)
395 {
396 let [ii,jj] = (this.board[i1][j1]==V.EMPTY ? [i1,j1] : [i2,j2]);
397 let [i3,j3] = [ii+step[0],jj+step[1]];
398 while (V.OnBoard(i3,j3) && this.board[i3][j3]==V.EMPTY)
399 {
400 i3 += step[0];
401 j3 += step[1];
402 }
403 if (V.OnBoard(i3,j3) && colors.includes(this.getColor(i3,j3))
404 && this.getPiece(i3,j3) == V.PAWN && !this.isImmobilized([i3,j3]))
405 {
406 return true;
407 }
408 }
409 }
410 }
411 }
412 return false;
413 }
414
415 isAttackedByRook([x,y], colors)
416 {
417 // King must be on same column or row,
418 // and a rook should be able to reach a capturing square
419 // colors contains only one element, giving the oppCol and thus king position
420 const sameRow = (x == this.kingPos[colors[0]][0]);
421 const sameColumn = (y == this.kingPos[colors[0]][1]);
422 if (sameRow || sameColumn)
423 {
424 // Look for the enemy rook (maximum 1)
425 for (let i=0; i<V.size.x; i++)
426 {
427 for (let j=0; j<V.size.y; j++)
428 {
429 if (this.board[i][j] != V.EMPTY && colors.includes(this.getColor(i,j))
430 && this.getPiece(i,j) == V.ROOK)
431 {
432 if (this.isImmobilized([i,j]))
433 return false; //because only one rook
434 // Can it reach a capturing square?
435 // Easy but quite suboptimal way (TODO): generate all moves (turn is OK)
436 const moves = this.getPotentialMovesFrom([i,j]);
437 for (let move of moves)
438 {
439 if (sameRow && move.end.y == y || sameColumn && move.end.x == x)
440 return true;
441 }
442 }
443 }
444 }
445 }
446 return false;
447 }
448
449 isAttackedByKnight([x,y], colors)
450 {
451 // Square (x,y) must be on same line as a knight,
452 // and there must be empty square(s) behind.
453 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
454 outerLoop:
455 for (let step of steps)
456 {
457 const [i0,j0] = [x+step[0],y+step[1]];
458 if (V.OnBoard(i0,j0) && this.board[i0][j0] == V.EMPTY)
459 {
460 // Try in opposite direction:
461 let [i,j] = [x-step[0],y-step[1]];
462 while (V.OnBoard(i,j))
463 {
464 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
465 {
466 i -= step[0];
467 j -= step[1];
468 }
469 if (V.OnBoard(i,j))
470 {
471 if (colors.includes(this.getColor(i,j)))
472 {
473 if (this.getPiece(i,j) == V.KNIGHT && !this.isImmobilized([i,j]))
474 return true;
475 continue outerLoop;
476 }
477 // [else] Our color, could be captured *if there was an empty space*
478 if (this.board[i+step[0]][j+step[1]] != V.EMPTY)
479 continue outerLoop;
480 i -= step[0];
481 j -= step[1];
482 }
483 }
484 }
485 }
486 return false;
487 }
488
489 isAttackedByBishop([x,y], colors)
490 {
491 // We cheat a little here: since this function is used exclusively for king,
492 // it's enough to check the immediate surrounding of the square.
493 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
494 for (let step of adjacentSteps)
495 {
496 const [i,j] = [x+step[0],y+step[1]];
497 if (V.OnBoard(i,j) && this.board[i][j]!=V.EMPTY
498 && colors.includes(this.getColor(i,j)) && this.getPiece(i,j) == V.BISHOP)
499 {
500 return true; //bishops are never immobilized
501 }
502 }
503 return false;
504 }
505
506 isAttackedByQueen([x,y], colors)
507 {
508 // Square (x,y) must be adjacent to a queen, and the queen must have
509 // some free space in the opposite direction from (x,y)
510 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
511 for (let step of adjacentSteps)
512 {
513 const sq2 = [x+2*step[0],y+2*step[1]];
514 if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY)
515 {
516 const sq1 = [x+step[0],y+step[1]];
517 if (this.board[sq1[0]][sq1[1]] != V.EMPTY
518 && colors.includes(this.getColor(sq1[0],sq1[1]))
519 && this.getPiece(sq1[0],sq1[1]) == V.QUEEN
520 && !this.isImmobilized(sq1))
521 {
522 return true;
523 }
524 }
525 }
526 return false;
527 }
528
529 static get VALUES()
530 {
531 // TODO: totally experimental!
532 return {
533 'p': 1,
534 'r': 2,
535 'n': 5,
536 'b': 3,
537 'q': 3,
538 'm': 5,
539 'k': 1000
540 };
541 }
542
543 static get SEARCH_DEPTH() { return 2; } //TODO?
544
545 static GenRandInitFen()
546 {
547 let pieces = { "w": new Array(8), "b": new Array(8) };
548 // Shuffle pieces on first and last rank
549 for (let c of ["w","b"])
550 {
551 let positions = range(8);
552 // Get random squares for every piece, totally freely
553
554 let randIndex = random(8);
555 const bishop1Pos = positions[randIndex];
556 positions.splice(randIndex, 1);
557
558 randIndex = random(7);
559 const bishop2Pos = positions[randIndex];
560 positions.splice(randIndex, 1);
561
562 randIndex = random(6);
563 const knight1Pos = positions[randIndex];
564 positions.splice(randIndex, 1);
565
566 randIndex = random(5);
567 const knight2Pos = positions[randIndex];
568 positions.splice(randIndex, 1);
569
570 randIndex = random(4);
571 const queenPos = positions[randIndex];
572 positions.splice(randIndex, 1);
573
574 randIndex = random(3);
575 const kingPos = positions[randIndex];
576 positions.splice(randIndex, 1);
577
578 randIndex = random(2);
579 const rookPos = positions[randIndex];
580 positions.splice(randIndex, 1);
581 const immobilizerPos = positions[0];
582
583 pieces[c][bishop1Pos] = 'b';
584 pieces[c][bishop2Pos] = 'b';
585 pieces[c][knight1Pos] = 'n';
586 pieces[c][knight2Pos] = 'n';
587 pieces[c][queenPos] = 'q';
588 pieces[c][kingPos] = 'k';
589 pieces[c][rookPos] = 'r';
590 pieces[c][immobilizerPos] = 'm';
591 }
592 return pieces["b"].join("") +
593 "/pppppppp/8/8/8/8/PPPPPPPP/" +
594 pieces["w"].join("").toUpperCase() +
595 " w";
596 }
597
598 getNotation(move)
599 {
600 const initialSquare = V.CoordsToSquare(move.start);
601 const finalSquare = V.CoordsToSquare(move.end);
602 let notation = undefined;
603 if (move.appear[0].p == V.PAWN)
604 {
605 // Pawn: generally ambiguous short notation, so we use full description
606 notation = "P" + initialSquare + finalSquare;
607 }
608 else if (move.appear[0].p == V.KING)
609 notation = "K" + (move.vanish.length>1 ? "x" : "") + finalSquare;
610 else
611 notation = move.appear[0].p.toUpperCase() + finalSquare;
612 if (move.vanish.length > 1 && move.appear[0].p != V.KING)
613 notation += "X"; //capture mark (not describing what is captured...)
614 return notation;
615 }
616 }