Debug movesCount for MarseilleChess, complete draft of Eightpieces variant
[vchess.git] / client / src / variants / Eightpieces.js
1 import { ArrayFun } from "@/utils/array";
2 import { randInt, shuffle } from "@/utils/alea";
3 import { ChessRules, PiPo, Move } from "@/base_rules";
4
5 export const VariantRules = class EightpiecesRules extends ChessRules {
6 static get JAILER() {
7 return "j";
8 }
9 static get SENTRY() {
10 return "s";
11 }
12 static get LANCER() {
13 return "l";
14 }
15
16 static get PIECES() {
17 return ChessRules.PIECES.concat([V.JAILER, V.SENTRY, V.LANCER]);
18 }
19
20 static get LANCER_DIRS() {
21 return {
22 'c': [-1, 0], //north
23 'd': [-1, 1], //N-E
24 'e': [0, 1], //east
25 'f': [1, 1], //S-E
26 'g': [1, 0], //south
27 'h': [1, -1], //S-W
28 'm': [0, -1], //west
29 'o': [-1, -1] //N-W
30 };
31 }
32
33 getPiece(i, j) {
34 const piece = this.board[i][j].charAt(1);
35 // Special lancer case: 8 possible orientations
36 if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
37 return piece;
38 }
39
40 getPpath(b) {
41 if ([V.JAILER, V.SENTRY].concat(Object.keys(V.LANCER_DIRS)).includes(b[1]))
42 return "Eightpieces/" + b;
43 return b;
44 }
45
46 static ParseFen(fen) {
47 const fenParts = fen.split(" ");
48 return Object.assign(ChessRules.ParseFen(fen), {
49 sentrypush: fenParts[5]
50 });
51 }
52
53 getFen() {
54 return super.getFen() + " " + this.getSentrypushFen();
55 }
56
57 getFenForRepeat() {
58 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
59 }
60
61 getSentrypushFen() {
62 const L = this.sentryPush.length;
63 if (!this.sentryPush[L-1]) return "-";
64 let res = "";
65 this.sentryPush[L-1].forEach(coords =>
66 res += V.CoordsToSquare(coords) + ",");
67 return res.slice(0, -1);
68 }
69
70 setOtherVariables(fen) {
71 super.setOtherVariables(fen);
72 // subTurn == 2 only when a sentry moved, and is about to push something
73 this.subTurn = 1;
74 // Stack pieces' forbidden squares after a sentry move at each turn
75 const parsedFen = V.ParseFen(fen);
76 if (parsedFen.sentrypush == "-") this.sentryPush = [null];
77 else {
78 this.sentryPush = [
79 parsedFen.sentrypush.split(",").map(sq => {
80 return V.SquareToCoords(sq);
81 })
82 ];
83 }
84 }
85
86 canTake([x1,y1], [x2, y2]) {
87 if (this.subTurn == 2)
88 // Sentry push: pieces can capture own color (only)
89 return this.getColor(x1, y1) == this.getColor(x2, y2);
90 return super.canTake([x1,y1], [x2, y2]);
91 }
92
93 static GenRandInitFen(randomness) {
94 if (randomness == 0)
95 // Deterministic:
96 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
97
98 let pieces = { w: new Array(8), b: new Array(8) };
99 // Shuffle pieces on first (and last rank if randomness == 2)
100 for (let c of ["w", "b"]) {
101 if (c == 'b' && randomness == 1) {
102 pieces['b'] = pieces['w'];
103 break;
104 }
105
106 let positions = ArrayFun.range(8);
107
108 // Get random squares for bishop and sentry
109 let randIndex = 2 * randInt(4);
110 let bishopPos = positions[randIndex];
111 // The sentry must be on a square of different color
112 let randIndex_tmp = 2 * randInt(4) + 1;
113 let sentryPos = positions[randIndex_tmp];
114 if (c == 'b') {
115 // Check if white sentry is on the same color as ours.
116 // If yes: swap bishop and sentry positions.
117 if ((pieces['w'].indexOf('s') - sentryPos) % 2 == 0)
118 [bishopPos, sentryPos] = [sentryPos, bishopPos];
119 }
120 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
121 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
122
123 // Get random squares for knight and lancer
124 randIndex = randInt(6);
125 const knightPos = positions[randIndex];
126 positions.splice(randIndex, 1);
127 randIndex = randInt(5);
128 const lancerPos = positions[randIndex];
129 positions.splice(randIndex, 1);
130
131 // Get random square for queen
132 randIndex = randInt(4);
133 const queenPos = positions[randIndex];
134 positions.splice(randIndex, 1);
135
136 // Rook, jailer and king positions are now almost fixed,
137 // only the ordering rook-> jailer or jailer->rook must be decided.
138 let rookPos = positions[0];
139 let jailerPos = positions[2];
140 const kingPos = positions[1];
141 if (Math.random() < 0.5) [rookPos, jailerPos] = [jailerPos, rookPos];
142
143 pieces[c][rookPos] = "r";
144 pieces[c][knightPos] = "n";
145 pieces[c][bishopPos] = "b";
146 pieces[c][queenPos] = "q";
147 pieces[c][kingPos] = "k";
148 pieces[c][sentryPos] = "s";
149 // Lancer faces north for white, and south for black:
150 pieces[c][lancerPos] = c == 'w' ? 'c' : 'g';
151 pieces[c][jailerPos] = "j";
152 }
153 return (
154 pieces["b"].join("") +
155 "/pppppppp/8/8/8/8/PPPPPPPP/" +
156 pieces["w"].join("").toUpperCase() +
157 " w 0 1111 - -"
158 );
159 }
160
161 // Scan kings, rooks and jailers
162 scanKingsRooks(fen) {
163 this.INIT_COL_KING = { w: -1, b: -1 };
164 this.INIT_COL_ROOK = { w: -1, b: -1 };
165 this.INIT_COL_JAILER = { w: -1, b: -1 };
166 this.kingPos = { w: [-1, -1], b: [-1, -1] };
167 const fenRows = V.ParseFen(fen).position.split("/");
168 const startRow = { 'w': V.size.x - 1, 'b': 0 };
169 for (let i = 0; i < fenRows.length; i++) {
170 let k = 0;
171 for (let j = 0; j < fenRows[i].length; j++) {
172 switch (fenRows[i].charAt(j)) {
173 case "k":
174 this.kingPos["b"] = [i, k];
175 this.INIT_COL_KING["b"] = k;
176 break;
177 case "K":
178 this.kingPos["w"] = [i, k];
179 this.INIT_COL_KING["w"] = k;
180 break;
181 case "r":
182 if (i == startRow['b'] && this.INIT_COL_ROOK["b"] < 0)
183 this.INIT_COL_ROOK["b"] = k;
184 break;
185 case "R":
186 if (i == startRow['w'] && this.INIT_COL_ROOK["w"] < 0)
187 this.INIT_COL_ROOK["w"] = k;
188 break;
189 case "j":
190 if (i == startRow['b'] && this.INIT_COL_JAILER["b"] < 0)
191 this.INIT_COL_JAILER["b"] = k;
192 break;
193 case "J":
194 if (i == startRow['w'] && this.INIT_COL_JAILER["w"] < 0)
195 this.INIT_COL_JAILER["w"] = k;
196 break;
197 default: {
198 const num = parseInt(fenRows[i].charAt(j));
199 if (!isNaN(num)) k += num - 1;
200 }
201 }
202 k++;
203 }
204 }
205 }
206
207 // Is piece on square (x,y) immobilized?
208 isImmobilized([x, y]) {
209 const color = this.getColor(x, y);
210 const oppCol = V.GetOppCol(color);
211 for (let step of V.steps[V.ROOK]) {
212 const [i, j] = [x + step[0], y + step[1]];
213 if (
214 V.OnBoard(i, j) &&
215 this.board[i][j] != V.EMPTY &&
216 this.getColor(i, j) == oppCol
217 ) {
218 const oppPiece = this.getPiece(i, j);
219 if (oppPiece == V.JAILER) return [i, j];
220 }
221 }
222 return null;
223 }
224
225 getPotentialMovesFrom_aux([x, y]) {
226 switch (this.getPiece(x, y)) {
227 case V.JAILER:
228 return this.getPotentialJailerMoves([x, y]);
229 case V.SENTRY:
230 return this.getPotentialSentryMoves([x, y]);
231 case V.LANCER:
232 return this.getPotentialLancerMoves([x, y]);
233 default:
234 return super.getPotentialMovesFrom([x, y]);
235 }
236 }
237
238 getPotentialMovesFrom([x,y]) {
239 if (this.subTurn == 1) {
240 if (!!this.isImmobilized([x, y])) return [];
241 return this.getPotentialMovesFrom_aux([x, y]);
242 }
243 // subTurn == 2: only the piece pushed by the sentry is allowed to move,
244 // as if the sentry didn't exist
245 if (x != this.sentryPos.x && y != this.sentryPos.y) return [];
246 return this.getPotentialMovesFrom_aux([x, y]);
247 }
248
249 getAllValidMoves() {
250 let moves = super.getAllValidMoves().filter(m => {
251 // Remove jailer captures
252 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
253 });
254 const L = this.sentryPush.length;
255 if (!!this.sentryPush[L-1] && this.subTurn == 1) {
256 // Delete moves walking back on sentry push path
257 moves = moves.filter(m => {
258 if (
259 m.vanish[0].p != V.PAWN &&
260 this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
261 ) {
262 return false;
263 }
264 return true;
265 });
266 }
267 return moves;
268 }
269
270 filterValid(moves) {
271 // Disable check tests when subTurn == 2, because the move isn't finished
272 if (this.subTurn == 2) return moves;
273 const filteredMoves = super.filterValid(moves);
274 // If at least one full move made, everything is allowed:
275 if (this.movesCount >= 2) return filteredMoves;
276 // Else, forbid check and captures:
277 const oppCol = V.GetOppCol(this.turn);
278 return filteredMoves.filter(m => {
279 if (m.vanish.length == 2 && m.appear.length == 1) return false;
280 this.play(m);
281 const res = !this.underCheck(oppCol);
282 this.undo(m);
283 return res;
284 });
285 }
286
287 // Obtain all lancer moves in "step" direction,
288 // without final re-orientation.
289 getPotentialLancerMoves_aux([x, y], step) {
290 let moves = [];
291 // Add all moves to vacant squares until opponent is met:
292 const oppCol = V.GetOppCol(this.turn);
293 let sq = [x + step[0], y + step[1]];
294 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
295 if (this.board[sq[0]][sq[1]] == V.EMPTY)
296 moves.push(this.getBasicMove([x, y], sq));
297 sq[0] += step[0];
298 sq[1] += step[1];
299 }
300 if (V.OnBoard(sq[0], sq[1]))
301 // Add capturing move
302 moves.push(this.getBasicMove([x, y], sq));
303 return moves;
304 }
305
306 getPotentialLancerMoves([x, y]) {
307 let moves = [];
308 // Add all lancer possible orientations, similar to pawn promotions.
309 // Except if just after a push: allow all movements from init square then
310 if (!!this.sentryPath[L-1]) {
311 // Maybe I was pushed
312 const pl = this.sentryPath[L-1].length;
313 if (
314 this.sentryPath[L-1][pl-1].x == x &&
315 this.sentryPath[L-1][pl-1].y == y
316 ) {
317 // I was pushed: allow all directions (for this move only), but
318 // do not change direction after moving.
319 Object.values(V.LANCER_DIRS).forEach(step => {
320 Array.prototype.push.apply(
321 moves,
322 this.getPotentialLancerMoves_aux([x, y], step)
323 );
324 });
325 return moves;
326 }
327 }
328 // I wasn't pushed: standard lancer move
329 const dirCode = this.board[x][y][1];
330 const monodirMoves =
331 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
332 // Add all possible orientations aftermove:
333 monodirMoves.forEach(m => {
334 Object.keys(V.LANCER_DIRS).forEach(k => {
335 let mk = JSON.parse(JSON.stringify(m));
336 mk.appear[0].p = k;
337 moves.push(mk);
338 });
339 });
340 return moves;
341 }
342
343 getPotentialSentryMoves([x, y]) {
344 // The sentry moves a priori like a bishop:
345 let moves = super.getPotentialBishopMoves([x, y]);
346 // ...but captures are replaced by special move
347 moves.forEach(m => {
348 if (m.vanish.length == 2) {
349 // Temporarily cancel the sentry capture:
350 m.appear.pop();
351 m.vanish.pop();
352 }
353 });
354 return moves;
355 }
356
357 getPotentialJailerMoves([x, y]) {
358 // Captures are removed afterward:
359 return super.getPotentialRookMoves([x, y]);
360 }
361
362 getPotentialKingMoves([x, y]) {
363 let moves = super.getPotentialKingMoves([x, y]);
364 // Augment with pass move is the king is immobilized:
365 const jsq = this.isImmobilized([x, y]);
366 if (!!jsq) {
367 moves.push(
368 new Move({
369 appear: [],
370 vanish: [],
371 start: { x: x, y: y },
372 end: { x: jsq[0], y: jsq[1] }
373 })
374 );
375 }
376 return moves;
377 }
378
379 // Adapted: castle with jailer possible
380 getCastleMoves([x, y]) {
381 const c = this.getColor(x, y);
382 const firstRank = (c == "w" ? V.size.x - 1 : 0);
383 if (x != firstRank || y != this.INIT_COL_KING[c])
384 return [];
385
386 const oppCol = V.GetOppCol(c);
387 let moves = [];
388 let i = 0;
389 // King, then rook or jailer:
390 const finalSquares = [
391 [2, 3],
392 [V.size.y - 2, V.size.y - 3]
393 ];
394 castlingCheck: for (
395 let castleSide = 0;
396 castleSide < 2;
397 castleSide++
398 ) {
399 if (!this.castleFlags[c][castleSide]) continue;
400 // Rook (or jailer) and king are on initial position
401
402 const finDist = finalSquares[castleSide][0] - y;
403 let step = finDist / Math.max(1, Math.abs(finDist));
404 i = y;
405 do {
406 if (
407 this.isAttacked([x, i], [oppCol]) ||
408 (this.board[x][i] != V.EMPTY &&
409 (this.getColor(x, i) != c ||
410 ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
411 ) {
412 continue castlingCheck;
413 }
414 i += step;
415 } while (i != finalSquares[castleSide][0]);
416
417 step = castleSide == 0 ? -1 : 1;
418 const rookOrJailerPos =
419 castleSide == 0
420 ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c])
421 : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]);
422 for (i = y + step; i != rookOrJailerPos; i += step)
423 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
424
425 // Nothing on final squares, except maybe king and castling rook or jailer?
426 for (i = 0; i < 2; i++) {
427 if (
428 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
429 this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
430 finalSquares[castleSide][i] != rookOrJailerPos
431 ) {
432 continue castlingCheck;
433 }
434 }
435
436 // If this code is reached, castle is valid
437 const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
438 moves.push(
439 new Move({
440 appear: [
441 new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
442 new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
443 ],
444 vanish: [
445 new PiPo({ x: x, y: y, p: V.KING, c: c }),
446 new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
447 ],
448 end:
449 Math.abs(y - rookOrJailerPos) <= 2
450 ? { x: x, y: rookOrJailerPos }
451 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
452 })
453 );
454 }
455
456 return moves;
457 }
458
459 updateVariables(move) {
460 super.updateVariables(move);
461 if (this.subTurn == 2) {
462 // A piece is pushed: forbid array of squares between start and end
463 // of move, included (except if it's a pawn)
464 let squares = [];
465 if (move.vanish[0].p != V.PAWN) {
466 if ([V.KNIGHT,V.KING].insludes(move.vanish[0].p))
467 // short-range pieces: just forbid initial square
468 squares.push(move.start);
469 else {
470 const deltaX = move.end.x - move.start.x;
471 const deltaY = move.end.y - move.start.y;
472 const step = [
473 deltaX / Math.abs(deltaX) || 0,
474 deltaY / Math.abs(deltaY) || 0
475 ];
476 for (
477 let sq = {x: x, y: y};
478 sq.x != move.end.x && sq.y != move.end.y;
479 sq.x += step[0], sq.y += step[1]
480 ) {
481 squares.push(sq);
482 }
483 }
484 // Add end square as well, to know if I was pushed (useful for lancers)
485 squares.push(move.end);
486 }
487 this.sentryPush.push(squares);
488 } else this.sentryPush.push(null);
489 }
490
491 play(move) {
492 move.flags = JSON.stringify(this.aggregateFlags());
493 this.epSquares.push(this.getEpSquare(move));
494 V.PlayOnBoard(this.board, move);
495 if (this.subTurn == 1) this.movesCount++;
496 this.updateVariables(move);
497 // move.sentryPush indicates that sentry is *about to* push
498 move.sentryPush = (move.appear.length == 0 && move.vanish.length == 1);
499 // Turn changes only if not a sentry "pre-push" or subTurn == 2 (push)
500 if (!move.sentryPush || this.subTurn == 2)
501 this.turn = V.GetOppCol(this.turn);
502 }
503
504 undo(move) {
505 this.epSquares.pop();
506 this.disaggregateFlags(JSON.parse(move.flags));
507 V.UndoOnBoard(this.board, move);
508 if (this.subTurn == 2) this.movesCount--;
509 this.unupdateVariables(move);
510 // Turn changes only if not undoing second part of a sentry push
511 if (!move.sentryPush || this.subTurn == 1)
512 this.turn = V.GetOppCol(this.turn);
513 }
514
515 static get VALUES() {
516 return Object.assign(
517 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
518 ChessRules.VALUES
519 );
520 }
521
522 getNotation(move) {
523 // Special case "king takes jailer" is a pass move
524 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
525 return super.getNotation(move);
526 }
527 };