Attempt to improve scrolling on smartphone
[vchess.git] / client / src / variants / Interweave.js
CommitLineData
6e47d367
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
3import { randInt, shuffle } from "@/utils/alea";
4
5export class InterweaveRules extends ChessRules {
7e8a7ea1 6
6e47d367
BA
7 static get HasFlags() {
8 return false;
9 }
10
4313762d
BA
11 static GenRandInitFen(options) {
12 if (options.randomness == 0)
6e47d367
BA
13 return "rbnkknbr/pppppppp/8/8/8/8/PPPPPPPP/RBNKKNBR w 0 - 000000";
14
15 let pieces = { w: new Array(8), b: new Array(8) };
16 for (let c of ["w", "b"]) {
4313762d 17 if (c == 'b' && options.randomness == 1) {
6e47d367
BA
18 pieces['b'] = pieces['w'];
19 break;
20 }
21
22 // Each pair of pieces on 2 colors:
23 const composition = ['r', 'n', 'b', 'k', 'r', 'n', 'b', 'k'];
24 let positions = shuffle(ArrayFun.range(4));
25 for (let i = 0; i < 4; i++)
26 pieces[c][2 * positions[i]] = composition[i];
27 positions = shuffle(ArrayFun.range(4));
28 for (let i = 0; i < 4; i++)
29 pieces[c][2 * positions[i] + 1] = composition[i];
30 }
31 return (
32 pieces["b"].join("") +
33 "/pppppppp/8/8/8/8/PPPPPPPP/" +
34 pieces["w"].join("").toUpperCase() +
35 // En-passant allowed, but no flags
36 " w 0 - 000000"
37 );
38 }
39
40 static IsGoodFen(fen) {
41 if (!ChessRules.IsGoodFen(fen)) return false;
42 const fenParsed = V.ParseFen(fen);
43 // 4) Check captures
44 if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{6,6}$/))
45 return false;
46 return true;
47 }
48
49 static IsGoodPosition(position) {
50 if (position.length == 0) return false;
51 const rows = position.split("/");
52 if (rows.length != V.size.x) return false;
53 let kings = { "k": 0, "K": 0 };
54 for (let row of rows) {
55 let sumElts = 0;
56 for (let i = 0; i < row.length; i++) {
57 if (['K','k'].includes(row[i])) kings[row[i]]++;
58 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
59 else {
e50a8025 60 const num = parseInt(row[i], 10);
6e47d367
BA
61 if (isNaN(num)) return false;
62 sumElts += num;
63 }
64 }
65 if (sumElts != V.size.y) return false;
66 }
67 // Both kings should be on board. Exactly two per color.
68 if (Object.values(kings).some(v => v != 2)) return false;
69 return true;
70 }
71
72 static ParseFen(fen) {
73 const fenParts = fen.split(" ");
74 return Object.assign(
75 ChessRules.ParseFen(fen),
76 { captured: fenParts[4] }
77 );
78 }
79
80 getFen() {
81 return super.getFen() + " " + this.getCapturedFen();
82 }
83
84 getFenForRepeat() {
85 return super.getFenForRepeat() + "_" + this.getCapturedFen();
86 }
87
88 getCapturedFen() {
89 let counts = [...Array(6).fill(0)];
90 [V.ROOK, V.KNIGHT, V.BISHOP].forEach((p,idx) => {
91 counts[idx] = this.captured["w"][p];
92 counts[3 + idx] = this.captured["b"][p];
93 });
94 return counts.join("");
95 }
96
97 scanKings() {}
98
99 setOtherVariables(fen) {
100 super.setOtherVariables(fen);
e50a8025
BA
101 const captured =
102 V.ParseFen(fen).captured.split("").map(x => parseInt(x, 10));
6e47d367
BA
103 // Initialize captured pieces' counts from FEN
104 this.captured = {
105 w: {
e50a8025
BA
106 [V.ROOK]: captured[0],
107 [V.KNIGHT]: captured[1],
108 [V.BISHOP]: captured[2]
6e47d367
BA
109 },
110 b: {
e50a8025
BA
111 [V.ROOK]: captured[3],
112 [V.KNIGHT]: captured[4],
113 [V.BISHOP]: captured[5]
6e47d367
BA
114 }
115 };
116 // Stack of "last move" only for intermediate captures
117 this.lastMoveEnd = [null];
118 }
119
120 // Trim all non-capturing moves
121 static KeepCaptures(moves) {
122 return moves.filter(m => m.vanish.length >= 2 || m.appear.length == 0);
123 }
124
125 // Stop at the first capture found (if any)
126 atLeastOneCapture() {
127 const color = this.turn;
128 for (let i = 0; i < V.size.x; i++) {
129 for (let j = 0; j < V.size.y; j++) {
130 if (
131 this.board[i][j] != V.EMPTY &&
132 this.getColor(i, j) == color &&
133 V.KeepCaptures(this.getPotentialMovesFrom([i, j])).length > 0
134 ) {
135 return true;
136 }
137 }
138 }
139 return false;
140 }
141
142 // En-passant after 2-sq jump
143 getEpSquare(moveOrSquare) {
144 if (!moveOrSquare) return undefined;
145 if (typeof moveOrSquare === "string") {
146 const square = moveOrSquare;
147 if (square == "-") return undefined;
148 // Enemy pawn initial column must be given too:
149 let res = [];
150 const epParts = square.split(",");
151 res.push(V.SquareToCoords(epParts[0]));
152 res.push(V.ColumnToCoord(epParts[1]));
153 return res;
154 }
155 // Argument is a move:
156 const move = moveOrSquare;
157 const [sx, ex, sy, ey] =
158 [move.start.x, move.end.x, move.start.y, move.end.y];
159 if (
160 move.vanish.length == 1 &&
161 this.getPiece(sx, sy) == V.PAWN &&
162 Math.abs(sx - ex) == 2 &&
163 Math.abs(sy - ey) == 2
164 ) {
165 return [
166 {
167 x: (ex + sx) / 2,
168 y: (ey + sy) / 2
169 },
170 // The arrival column must be remembered, because
171 // potentially two pawns could be candidates to be captured:
172 // one on our left, and one on our right.
173 move.end.y
174 ];
175 }
176 return undefined; //default
177 }
178
179 static IsGoodEnpassant(enpassant) {
180 if (enpassant != "-") {
181 const epParts = enpassant.split(",");
182 const epSq = V.SquareToCoords(epParts[0]);
183 if (isNaN(epSq.x) || isNaN(epSq.y) || !V.OnBoard(epSq)) return false;
184 const arrCol = V.ColumnToCoord(epParts[1]);
185 if (isNaN(arrCol) || arrCol < 0 || arrCol >= V.size.y) return false;
186 }
187 return true;
188 }
189
190 getEnpassantFen() {
191 const L = this.epSquares.length;
192 if (!this.epSquares[L - 1]) return "-"; //no en-passant
193 return (
194 V.CoordsToSquare(this.epSquares[L - 1][0]) +
195 "," +
196 V.CoordToColumn(this.epSquares[L - 1][1])
197 );
198 }
199
7ddfec38 200 getPotentialMovesFrom([x, y]) {
6e47d367
BA
201 switch (this.getPiece(x, y)) {
202 case V.PAWN:
7ddfec38 203 return this.getPotentialPawnMoves([x, y]);
6e47d367 204 case V.ROOK:
7ddfec38 205 return this.getPotentialRookMoves([x, y]);
6e47d367 206 case V.KNIGHT:
7ddfec38 207 return this.getPotentialKnightMoves([x, y]);
6e47d367 208 case V.BISHOP:
7ddfec38 209 return this.getPotentialBishopMoves([x, y]);
6e47d367 210 case V.KING:
7ddfec38 211 return this.getPotentialKingMoves([x, y]);
6e47d367
BA
212 // No queens
213 }
7ddfec38 214 return [];
6e47d367
BA
215 }
216
217 // Special pawns movements
218 getPotentialPawnMoves([x, y]) {
219 const color = this.turn;
220 const oppCol = V.GetOppCol(color);
221 let moves = [];
222 const [sizeX, sizeY] = [V.size.x, V.size.y];
223 const shiftX = color == "w" ? -1 : 1;
224 const startRank = color == "w" ? sizeX - 2 : 1;
225 const potentialFinalPieces =
226 [V.ROOK, V.KNIGHT, V.BISHOP].filter(p => this.captured[color][p] > 0);
227 const lastRanks = (color == "w" ? [0, 1] : [sizeX - 1, sizeX - 2]);
228 if (x + shiftX == lastRanks[0] && potentialFinalPieces.length == 0)
229 // If no captured piece is available, the pawn cannot promote
230 return [];
231
232 const finalPieces1 =
233 x + shiftX == lastRanks[0]
234 ? potentialFinalPieces
235 :
236 x + shiftX == lastRanks[1]
237 ? potentialFinalPieces.concat([V.PAWN])
238 : [V.PAWN];
239 // One square diagonally
240 for (let shiftY of [-1, 1]) {
241 if (this.board[x + shiftX][y + shiftY] == V.EMPTY) {
242 for (let piece of finalPieces1) {
243 moves.push(
244 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
245 c: color,
246 p: piece
247 })
248 );
249 }
250 if (
251 V.PawnSpecs.twoSquares &&
252 x == startRank &&
253 y + 2 * shiftY >= 0 &&
254 y + 2 * shiftY < sizeY &&
255 this.board[x + 2 * shiftX][y + 2 * shiftY] == V.EMPTY
256 ) {
257 // Two squares jump
258 moves.push(
259 this.getBasicMove([x, y], [x + 2 * shiftX, y + 2 * shiftY])
260 );
261 }
262 }
263 }
264 // Capture
265 const finalPieces2 =
266 x + 2 * shiftX == lastRanks[0]
267 ? potentialFinalPieces
268 :
269 x + 2 * shiftX == lastRanks[1]
270 ? potentialFinalPieces.concat([V.PAWN])
271 : [V.PAWN];
272 if (
273 this.board[x + shiftX][y] != V.EMPTY &&
274 this.canTake([x, y], [x + shiftX, y]) &&
275 V.OnBoard(x + 2 * shiftX, y) &&
276 this.board[x + 2 * shiftX][y] == V.EMPTY
277 ) {
278 const oppPiece = this.getPiece(x + shiftX, y);
279 for (let piece of finalPieces2) {
280 let mv = this.getBasicMove(
281 [x, y], [x + 2 * shiftX, y], { c: color, p: piece });
282 mv.vanish.push({
283 x: x + shiftX,
284 y: y,
285 p: oppPiece,
286 c: oppCol
287 });
288 moves.push(mv);
289 }
290 }
291
292 // En passant
293 const Lep = this.epSquares.length;
294 const epSquare = this.epSquares[Lep - 1]; //always at least one element
295 if (
296 !!epSquare &&
297 epSquare[0].x == x + shiftX &&
298 epSquare[0].y == y &&
299 this.board[x + 2 * shiftX][y] == V.EMPTY
300 ) {
301 for (let piece of finalPieces2) {
302 let enpassantMove =
303 this.getBasicMove(
304 [x, y], [x + 2 * shiftX, y], { c: color, p: piece});
305 enpassantMove.vanish.push({
306 x: x,
307 y: epSquare[1],
308 p: "p",
309 c: this.getColor(x, epSquare[1])
310 });
311 moves.push(enpassantMove);
312 }
313 }
314
315 // Add custodian captures:
316 const steps = V.steps[V.ROOK];
317 moves.forEach(m => {
318 // Try capturing in every direction
319 for (let step of steps) {
320 const sq2 = [m.end.x + 2 * step[0], m.end.y + 2 * step[1]];
321 if (
322 V.OnBoard(sq2[0], sq2[1]) &&
323 this.board[sq2[0]][sq2[1]] != V.EMPTY &&
324 this.getColor(sq2[0], sq2[1]) == color
325 ) {
326 // Potential capture
327 const sq1 = [m.end.x + step[0], m.end.y + step[1]];
328 if (
329 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
330 this.getColor(sq1[0], sq1[1]) == oppCol
331 ) {
332 m.vanish.push(
333 new PiPo({
334 x: sq1[0],
335 y: sq1[1],
336 c: oppCol,
337 p: this.getPiece(sq1[0], sq1[1])
338 })
339 );
340 }
341 }
342 }
343 });
344
345 return moves;
346 }
347
348 getSlides([x, y], steps, options) {
349 options = options || {};
350 // No captures:
351 let moves = [];
352 outerLoop: for (let step of steps) {
353 let i = x + step[0];
354 let j = y + step[1];
355 let counter = 1;
356 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
357 if (!options["doubleStep"] || counter % 2 == 0)
358 moves.push(this.getBasicMove([x, y], [i, j]));
359 if (!!options["oneStep"]) continue outerLoop;
360 i += step[0];
361 j += step[1];
362 counter++;
363 }
364 }
365 return moves;
366 }
367
368 // Smasher
369 getPotentialRookMoves([x, y]) {
370 let moves =
371 this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true })
372 .concat(this.getSlides([x, y], V.steps[V.BISHOP]));
373 // Add captures
374 const oppCol = V.GetOppCol(this.turn);
375 moves.forEach(m => {
376 const delta = [m.end.x - m.start.x, m.end.y - m.start.y];
377 const step = [
378 delta[0] / Math.abs(delta[0]) || 0,
379 delta[1] / Math.abs(delta[1]) || 0
380 ];
381 if (step[0] == 0 || step[1] == 0) {
382 // Rook-like move, candidate for capturing
383 const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
384 if (
385 V.OnBoard(i, j) &&
386 this.board[i][j] != V.EMPTY &&
387 this.getColor(i, j) == oppCol
388 ) {
389 m.vanish.push({
390 x: i,
391 y: j,
392 p: this.getPiece(i, j),
393 c: oppCol
394 });
395 }
396 }
397 });
398 return moves;
399 }
400
401 // Leaper
402 getPotentialKnightMoves([x, y]) {
403 let moves =
404 this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true })
405 .concat(this.getSlides([x, y], V.steps[V.BISHOP]));
406 const oppCol = V.GetOppCol(this.turn);
407 // Look for double-knight moves (could capture):
408 for (let step of V.steps[V.KNIGHT]) {
409 const [i, j] = [x + 2 * step[0], y + 2 * step[1]];
410 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
411 const [ii, jj] = [x + step[0], y + step[1]];
412 if (this.board[ii][jj] == V.EMPTY || this.getColor(ii, jj) == oppCol) {
413 let mv = this.getBasicMove([x, y], [i, j]);
414 if (this.board[ii][jj] != V.EMPTY) {
415 mv.vanish.push({
416 x: ii,
417 y: jj,
418 c: oppCol,
419 p: this.getPiece(ii, jj)
420 });
421 }
422 moves.push(mv);
423 }
424 }
425 }
426 // Look for an enemy in every orthogonal direction
427 for (let step of V.steps[V.ROOK]) {
428 let [i, j] = [x + step[0], y+ step[1]];
429 let counter = 1;
430 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
431 i += step[0];
432 j += step[1];
433 counter++;
434 }
435 if (
436 V.OnBoard(i, j) &&
437 counter % 2 == 1 &&
438 this.getColor(i, j) == oppCol
439 ) {
440 const oppPiece = this.getPiece(i, j);
441 // Candidate for capture: can I land after?
442 let [ii, jj] = [i + step[0], j + step[1]];
443 counter++;
444 while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) {
445 if (counter % 2 == 0) {
446 // Same color: add capture
447 let mv = this.getBasicMove([x, y], [ii, jj]);
448 mv.vanish.push({
449 x: i,
450 y: j,
451 c: oppCol,
452 p: oppPiece
453 });
454 moves.push(mv);
455 }
456 ii += step[0];
457 jj += step[1];
458 counter++;
459 }
460 }
461 }
462 return moves;
463 }
464
465 // Remover
466 getPotentialBishopMoves([x, y]) {
467 let moves = this.getSlides([x, y], V.steps[V.BISHOP]);
468 // Add captures
469 const oppCol = V.GetOppCol(this.turn);
470 let captures = [];
471 for (let step of V.steps[V.ROOK]) {
472 const [i, j] = [x + step[0], y + step[1]];
473 if (
474 V.OnBoard(i, j) &&
475 this.board[i][j] != V.EMPTY &&
476 this.getColor(i, j) == oppCol
477 ) {
478 captures.push([i, j]);
479 }
480 }
481 captures.forEach(c => {
482 moves.push({
483 start: { x: x, y: y },
484 end: { x: c[0], y: c[1] },
485 appear: [],
486 vanish: captures.map(ct => {
487 return {
488 x: ct[0],
489 y: ct[1],
490 c: oppCol,
491 p: this.getPiece(ct[0], ct[1])
492 };
493 })
494 });
495 });
496 return moves;
497 }
498
499 getPotentialKingMoves([x, y]) {
500 let moves = this.getSlides([x, y], V.steps[V.BISHOP], { oneStep: true });
501 // Add captures
502 const oppCol = V.GetOppCol(this.turn);
503 for (let step of V.steps[V.ROOK]) {
504 const [i, j] = [x + 2 * step[0], y + 2 * step[1]];
505 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
506 const [ii, jj] = [x + step[0], y + step[1]];
507 if (this.board[ii][jj] != V.EMPTY && this.getColor(ii, jj) == oppCol) {
508 let mv = this.getBasicMove([x, y], [i, j]);
509 mv.vanish.push({
510 x: ii,
511 y: jj,
512 c: oppCol,
513 p: this.getPiece(ii, jj)
514 });
515 moves.push(mv);
516 }
517 }
518 }
519 return moves;
520 }
521
522 getPossibleMovesFrom(sq) {
523 const L = this.lastMoveEnd.length;
524 if (
525 !!this.lastMoveEnd[L-1] &&
526 (
527 sq[0] != this.lastMoveEnd[L-1].x ||
528 sq[1] != this.lastMoveEnd[L-1].y
529 )
530 ) {
531 return [];
532 }
533 let moves = this.getPotentialMovesFrom(sq);
534 const captureMoves = V.KeepCaptures(moves);
535 if (captureMoves.length > 0) return captureMoves;
536 if (this.atLeastOneCapture()) return [];
537 return moves;
538 }
539
540 getAllValidMoves() {
541 const moves = this.getAllPotentialMoves();
542 const captures = V.KeepCaptures(moves);
543 if (captures.length > 0) return captures;
544 return moves;
545 }
546
547 filterValid(moves) {
548 // No checks
549 return moves;
550 }
551
552 play(move) {
553 this.epSquares.push(this.getEpSquare(move));
554 V.PlayOnBoard(this.board, move);
555 if (move.vanish.length >= 2) {
556 // Capture: update this.captured
557 for (let i=1; i<move.vanish.length; i++)
558 this.captured[move.vanish[i].c][move.vanish[i].p]++;
559 }
7ddfec38
BA
560 // Check if the move is the last of the turn
561 if (move.vanish.length >= 2 || move.appear.length == 0) {
562 const moreCaptures = (
563 V.KeepCaptures(
564 this.getPotentialMovesFrom([move.end.x, move.end.y])
565 )
566 .length > 0
567 );
568 move.last = !moreCaptures;
569 }
570 else move.last = true;
6e47d367
BA
571 if (!!move.last) {
572 // No capture, or no more capture available
573 this.turn = V.GetOppCol(this.turn);
574 this.movesCount++;
575 this.lastMoveEnd.push(null);
7ddfec38 576 move.last = true; //will be used in undo and computer play
6e47d367
BA
577 }
578 else this.lastMoveEnd.push(move.end);
579 }
580
581 undo(move) {
582 this.epSquares.pop();
583 this.lastMoveEnd.pop();
584 V.UndoOnBoard(this.board, move);
585 if (move.vanish.length >= 2) {
586 for (let i=1; i<move.vanish.length; i++)
587 this.captured[move.vanish[i].c][move.vanish[i].p]--;
588 }
589 if (!!move.last) {
590 this.turn = V.GetOppCol(this.turn);
591 this.movesCount--;
592 }
593 }
594
595 getCheckSquares() {
596 return [];
597 }
598
599 getCurrentScore() {
600 // Count kings: if one is missing, the side lost
601 let kingsCount = { 'w': 0, 'b': 0 };
602 for (let i=0; i<8; i++) {
603 for (let j=0; j<8; j++) {
604 if (this.board[i][j] != V.EMPTY && this.getPiece(i, j) == V.KING)
605 kingsCount[this.getColor(i, j)]++;
606 }
607 }
608 if (kingsCount['w'] < 2) return "0-1";
609 if (kingsCount['b'] < 2) return "1-0";
610 return "*";
611 }
612
613 getComputerMove() {
614 let moves = this.getAllValidMoves();
615 if (moves.length == 0) return null;
616 // Just play random moves (for now at least. TODO?)
617 let mvArray = [];
618 while (moves.length > 0) {
619 const mv = moves[randInt(moves.length)];
620 mvArray.push(mv);
7ddfec38 621 this.play(mv);
6e47d367 622 if (!mv.last) {
6e47d367
BA
623 moves = V.KeepCaptures(
624 this.getPotentialMovesFrom([mv.end.x, mv.end.y]));
625 }
626 else break;
627 }
7ddfec38 628 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
6e47d367
BA
629 return (mvArray.length > 1 ? mvArray : mvArray[0]);
630 }
631
632 getNotation(move) {
633 const initialSquare = V.CoordsToSquare(move.start);
634 const finalSquare = V.CoordsToSquare(move.end);
635 if (move.appear.length == 0)
636 // Remover captures 'R'
637 return initialSquare + "R";
638 let notation = move.appear[0].p.toUpperCase() + finalSquare;
639 // Add a capture mark (not describing what is captured...):
640 if (move.vanish.length >= 2) notation += "X";
641 return notation;
642 }
7e8a7ea1 643
6e47d367 644};