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