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