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