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