Draft Checkered + fix getBoard and Capablanca chess
[xogo.git] / variants / Checkered / class.js
1 import ChessRules from "/base_rules.js";
2 import PiPo from "/utils/PiPo.js";
3 import Move from "/utils/Move.js";
4
5 export default class CheckeredRules extends ChessRules {
6
7 static get Options() {
8 return {
9 select: C.Options.select,
10 input: [
11 {
12 label: "Allow switching",
13 variable: "withswitch",
14 type: "checkbox",
15 defaut: false
16 }
17 ],
18 // Game modifiers (using "elementary variants"). Default: false
19 styles: [
20 "balance",
21 "capture",
22 "cylinder",
23 "doublemove",
24 "madrasi",
25 "progressive",
26 "recycle",
27 "teleport"
28 ]
29 };
30 }
31
32 static board2fen(b) {
33 const checkered_codes = {
34 p: "s",
35 q: "t",
36 r: "u",
37 b: "c",
38 n: "o"
39 };
40 if (b[0] == "c") return checkered_codes[b[1]];
41 return super.board2fen(b);
42 }
43
44 static fen2board(f) {
45 // Tolerate upper-case versions of checkered pieces (why not?)
46 const checkered_pieces = {
47 s: "p",
48 S: "p",
49 t: "q",
50 T: "q",
51 u: "r",
52 U: "r",
53 c: "b",
54 C: "b",
55 o: "n",
56 O: "n"
57 };
58 if (Object.keys(checkered_pieces).includes(f))
59 return "c" + checkered_pieces[f];
60 return super.fen2board(f);
61 }
62
63 // TODO: pieces()
64
65 setOtherVariables(fenParsed) {
66 super.setOtherVariables(fenParsed);
67 // Non-capturing last checkered move (if any)
68 const cmove = fenParsed.cmove;
69 if (cmove == "-") this.cmove = null;
70 else {
71 this.cmove = {
72 start: C.SquareToCoords(cmove.substr(0, 2)),
73 end: C.SquareToCoords(cmove.substr(2))
74 };
75 }
76 // Stage 1: as Checkered2. Stage 2: checkered pieces are autonomous
77 const stageInfo = fenParsed.stage;
78 this.stage = parseInt(stageInfo[0], 10);
79 this.canSwitch = (this.stage == 1 && stageInfo[1] != '-');
80 this.sideCheckered = (this.stage == 2 ? stageInfo[1] : undefined);
81 }
82
83 setFlags(fenflags) {
84 super.setFlags(fenflags); //castleFlags
85 this.pawnFlags = {
86 w: [...Array(8)], //pawns can move 2 squares?
87 b: [...Array(8)]
88 };
89 const flags = fenflags.substr(4); //skip first 4 letters, for castle
90 for (let c of ["w", "b"]) {
91 for (let i = 0; i < 8; i++)
92 this.pawnFlags[c][i] = flags.charAt((c == "w" ? 0 : 8) + i) == "1";
93 }
94 }
95
96 aggregateFlags() {
97 return [this.castleFlags, this.pawnFlags];
98 }
99
100 disaggregateFlags(flags) {
101 this.castleFlags = flags[0];
102 this.pawnFlags = flags[1];
103 }
104
105 getEpSquare(moveOrSquare) {
106 // At stage 2, all pawns can be captured en-passant
107 if (
108 this.stage == 2 ||
109 typeof moveOrSquare !== "object" ||
110 (moveOrSquare.appear.length > 0 && moveOrSquare.appear[0].c != 'c')
111 )
112 return super.getEpSquare(moveOrSquare);
113 // Checkered or switch move: no en-passant
114 return undefined;
115 }
116
117 getCmove(move) {
118 // No checkered move to undo at stage 2:
119 if (this.stage == 1 && move.vanish.length == 1 && move.appear[0].c == "c")
120 return { start: move.start, end: move.end };
121 return null;
122 }
123
124 canTake([x1, y1], [x2, y2]) {
125 const color1 = this.getColor(x1, y1);
126 const color2 = this.getColor(x2, y2);
127 if (this.stage == 2) {
128 // Black & White <-- takes --> Checkered
129 const color1 = this.getColor(x1, y1);
130 const color2 = this.getColor(x2, y2);
131 return color1 != color2 && [color1, color2].includes('c');
132 }
133 // Checkered aren't captured
134 return (
135 color1 != color2 &&
136 color2 != "c" &&
137 (color1 != "c" || color2 != this.turn)
138 );
139 }
140
141 // TODO::::
142 getPotentialMovesFrom([x, y], noswitch) {
143 let standardMoves = super.getPotentialMovesFrom([x, y]);
144 if (this.stage == 1) {
145 const color = this.turn;
146 // Post-processing: apply "checkerization" of standard moves
147 const lastRank = (color == "w" ? 0 : 7);
148 let moves = [];
149 // King is treated differently: it never turn checkered
150 if (this.getPiece(x, y) == V.KING) {
151 // If at least one checkered piece, allow switching:
152 if (
153 this.canSwitch && !noswitch &&
154 this.board.some(b => b.some(cell => cell[0] == 'c'))
155 ) {
156 const oppCol = V.GetOppCol(color);
157 moves.push(
158 new Move({
159 start: { x: x, y: y },
160 end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] },
161 appear: [],
162 vanish: []
163 })
164 );
165 }
166 return standardMoves.concat(moves);
167 }
168 standardMoves.forEach(m => {
169 if (m.vanish[0].p == V.PAWN) {
170 if (
171 Math.abs(m.end.x - m.start.x) == 2 &&
172 !this.pawnFlags[this.turn][m.start.y]
173 ) {
174 return; //skip forbidden 2-squares jumps
175 }
176 if (
177 this.board[m.end.x][m.end.y] == V.EMPTY &&
178 m.vanish.length == 2 &&
179 this.getColor(m.start.x, m.start.y) == "c"
180 ) {
181 return; //checkered pawns cannot take en-passant
182 }
183 }
184 if (m.vanish.length == 1)
185 // No capture
186 moves.push(m);
187 else {
188 // A capture occured (m.vanish.length == 2)
189 m.appear[0].c = "c";
190 moves.push(m);
191 if (
192 // Avoid promotions (already treated):
193 m.appear[0].p != m.vanish[1].p &&
194 (m.vanish[0].p != V.PAWN || m.end.x != lastRank)
195 ) {
196 // Add transformation into captured piece
197 let m2 = JSON.parse(JSON.stringify(m));
198 m2.appear[0].p = m.vanish[1].p;
199 moves.push(m2);
200 }
201 }
202 });
203 return moves;
204 }
205 return standardMoves;
206 }
207
208 // TODO: merge this into pieces() method
209 getPotentialPawnMoves([x, y]) {
210 const color = this.getColor(x, y);
211 if (this.stage == 2) {
212 const saveTurn = this.turn;
213 if (this.sideCheckered == this.turn) {
214 // Cannot change PawnSpecs.bidirectional, so cheat a little:
215 this.turn = 'w';
216 const wMoves = super.getPotentialPawnMoves([x, y]);
217 this.turn = 'b';
218 const bMoves = super.getPotentialPawnMoves([x, y]);
219 this.turn = saveTurn;
220 return wMoves.concat(bMoves);
221 }
222 // Playing with both colors:
223 this.turn = color;
224 const moves = super.getPotentialPawnMoves([x, y]);
225 this.turn = saveTurn;
226 return moves;
227 }
228 let moves = super.getPotentialPawnMoves([x, y]);
229 // Post-process: set right color for checkered moves
230 if (color == 'c') {
231 moves.forEach(m => {
232 m.appear[0].c = 'c'; //may be done twice if capture
233 m.vanish[0].c = 'c';
234 });
235 }
236 return moves;
237 }
238
239 canIplay(side, [x, y]) {
240 if (this.stage == 2) {
241 const color = this.getColor(x, y);
242 return (
243 this.turn == this.sideCheckered
244 ? color == 'c'
245 : ['w', 'b'].includes(color)
246 );
247 }
248 return side == this.turn && [side, "c"].includes(this.getColor(x, y));
249 }
250
251 // Does m2 un-do m1 ? (to disallow undoing checkered moves)
252 oppositeMoves(m1, m2) {
253 return (
254 !!m1 &&
255 m2.appear[0].c == "c" &&
256 m2.appear.length == 1 &&
257 m2.vanish.length == 1 &&
258 m1.start.x == m2.end.x &&
259 m1.end.x == m2.start.x &&
260 m1.start.y == m2.end.y &&
261 m1.end.y == m2.start.y
262 );
263 }
264
265 // TODO: adapt, merge
266 filterValid(moves) {
267 if (moves.length == 0) return [];
268 const color = this.turn;
269 const oppCol = V.GetOppCol(color);
270 const L = this.cmoves.length; //at least 1: init from FEN
271 const stage = this.stage; //may change if switch
272 return moves.filter(m => {
273 // Checkered cannot be under check (no king)
274 if (stage == 2 && this.sideCheckered == color) return true;
275 this.play(m);
276 let res = true;
277 if (stage == 1) {
278 if (m.appear.length == 0 && m.vanish.length == 0) {
279 // Special "switch" move: kings must not be attacked by checkered.
280 // Not checking for oppositeMoves here: checkered are autonomous
281 res = (
282 !this.isAttacked(this.kingPos['w'], ['c']) &&
283 !this.isAttacked(this.kingPos['b'], ['c']) &&
284 this.getAllPotentialMoves().length > 0
285 );
286 }
287 else res = !this.oppositeMoves(this.cmoves[L - 1], m);
288 }
289 if (res && m.appear.length > 0) res = !this.underCheck(color);
290 // At stage 2, side with B & W can be undercheck with both kings:
291 if (res && stage == 2) res = !this.underCheck(oppCol);
292 this.undo(m);
293 return res;
294 });
295 }
296
297 atLeastOneMove() {
298 const color = this.turn;
299 const oppCol = V.GetOppCol(color);
300 for (let i = 0; i < V.size.x; i++) {
301 for (let j = 0; j < V.size.y; j++) {
302 const colIJ = this.getColor(i, j);
303 if (
304 this.board[i][j] != V.EMPTY &&
305 (
306 (this.stage == 1 && colIJ != oppCol) ||
307 (this.stage == 2 &&
308 (
309 (this.sideCheckered == color && colIJ == 'c') ||
310 (this.sideCheckered != color && ['w', 'b'].includes(colIJ))
311 )
312 )
313 )
314 ) {
315 const moves = this.getPotentialMovesFrom([i, j], "noswitch");
316 if (moves.length > 0) {
317 for (let k = 0; k < moves.length; k++)
318 if (this.filterValid([moves[k]]).length > 0) return true;
319 }
320 }
321 }
322 }
323 return false;
324 }
325
326 // TODO: adapt
327 underCheck(color) {
328 if (this.stage == 1)
329 return this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"]);
330 if (color == this.sideCheckered) return false;
331 return (
332 this.isAttacked(this.kingPos['w'], ["c"]) ||
333 this.isAttacked(this.kingPos['b'], ["c"])
334 );
335 }
336
337 play(move) {
338 move.flags = JSON.stringify(this.aggregateFlags());
339 this.epSquares.push(this.getEpSquare(move));
340 V.PlayOnBoard(this.board, move);
341 if (move.appear.length > 0 || move.vanish.length > 0)
342 {
343 this.turn = V.GetOppCol(this.turn);
344 this.movesCount++;
345 }
346 this.postPlay(move);
347 }
348
349 postPlay(move) {
350 if (move.appear.length == 0 && move.vanish.length == 0) {
351 this.stage = 2;
352 this.sideCheckered = this.turn;
353 }
354 else {
355 const c = move.vanish[0].c;
356 const piece = move.vanish[0].p;
357 if (piece == V.KING) {
358 this.kingPos[c][0] = move.appear[0].x;
359 this.kingPos[c][1] = move.appear[0].y;
360 }
361 super.updateCastleFlags(move, piece);
362 if (
363 [1, 6].includes(move.start.x) &&
364 move.vanish[0].p == V.PAWN &&
365 Math.abs(move.end.x - move.start.x) == 2
366 ) {
367 // This move turns off a 2-squares pawn flag
368 this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
369 }
370 }
371 this.cmove = this.getCmove(move);
372 }
373
374 getCurrentScore() {
375 const color = this.turn;
376 if (this.stage == 1) {
377 if (this.atLeastOneMove()) return "*";
378 // Artifically change turn, for checkered pawns
379 this.turn = V.GetOppCol(this.turn);
380 const res =
381 this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"])
382 ? color == "w"
383 ? "0-1"
384 : "1-0"
385 : "1/2";
386 this.turn = V.GetOppCol(this.turn);
387 return res;
388 }
389 // Stage == 2:
390 if (this.sideCheckered == this.turn) {
391 // Check if remaining checkered pieces: if none, I lost
392 if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
393 if (!this.atLeastOneMove()) return "1/2";
394 return "*";
395 }
396 return color == 'w' ? "0-1" : "1-0";
397 }
398 if (this.atLeastOneMove()) return "*";
399 let res = this.isAttacked(this.kingPos['w'], ["c"]);
400 if (!res) res = this.isAttacked(this.kingPos['b'], ["c"]);
401 if (res) return color == 'w' ? "0-1" : "1-0";
402 return "1/2";
403 }
404
405 // TODO: adapt
406 static GenRandInitFen(options) {
407 const baseFen = ChessRules.GenRandInitFen(options);
408 return (
409 // Add 16 pawns flags + empty cmove + stage == 1:
410 baseFen.slice(0, -2) + "1111111111111111 - - 1" +
411 (!options["switch"] ? '-' : "")
412 );
413 }
414 {
415 cmove: fenParts[5],
416 stage: fenParts[6]
417 }
418
419 getCmoveFen() {
420 const L = this.cmoves.length;
421 return (
422 !this.cmoves[L - 1]
423 ? "-"
424 : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
425 ChessRules.CoordsToSquare(this.cmoves[L - 1].end)
426 );
427 }
428
429 getStageFen() {
430 if (this.stage == 1) return "1" + (!this.canSwitch ? '-' : "");
431 // Stage == 2:
432 return "2" + this.sideCheckered;
433 }
434
435 getFen() {
436 return (
437 super.getFen() + " " + this.getCmoveFen() + " " + this.getStageFen()
438 );
439 }
440
441 getFlagsFen() {
442 let fen = super.getFlagsFen();
443 // Add pawns flags
444 for (let c of ["w", "b"])
445 for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
446 return fen;
447 }
448
449 };