| 1 | class CheckeredRules extends ChessRules |
| 2 | { |
| 3 | // Path to pieces |
| 4 | static getPpath(b) |
| 5 | { |
| 6 | return b[0]=='c' ? "Checkered/"+b : b; |
| 7 | } |
| 8 | static board2fen(b) |
| 9 | { |
| 10 | const checkered_codes = { |
| 11 | 'p': 's', |
| 12 | 'q': 't', |
| 13 | 'r': 'u', |
| 14 | 'b': 'c', |
| 15 | 'n': 'o', |
| 16 | }; |
| 17 | if (b[0]=="c") |
| 18 | return checkered_codes[b[1]]; |
| 19 | return ChessRules.board2fen(b); |
| 20 | } |
| 21 | static fen2board(f) |
| 22 | { |
| 23 | const checkered_pieces = { |
| 24 | 's': 'p', |
| 25 | 't': 'q', |
| 26 | 'u': 'r', |
| 27 | 'c': 'b', |
| 28 | 'o': 'n', |
| 29 | }; |
| 30 | if (Object.keys(checkered_pieces).includes(f)) |
| 31 | return 'c'+checkered_pieces[f]; |
| 32 | return ChessRules.fen2board(f); |
| 33 | } |
| 34 | |
| 35 | initVariables(fen) |
| 36 | { |
| 37 | super.initVariables(fen); |
| 38 | // Decode last non-capturing checkered move (if any) |
| 39 | // TODO: since now we store moves list, this can disappear |
| 40 | const cmove = fen.split(" ")[4]; |
| 41 | if (cmove != "-") |
| 42 | { |
| 43 | const piece = cmove.charAt(0); |
| 44 | const startEnd = cmove.substr(1).split(";"); |
| 45 | const start = startEnd[0].split(","); |
| 46 | const end = startEnd[1].split(","); |
| 47 | this.moves.push(new Move({ |
| 48 | appear: [ new PiPo({c:"c", p:piece, x:end[0], y:end[1]}) ], |
| 49 | vanish: [ new PiPo({c:"c", p:piece, x:start[0], y:start[1]}) ] |
| 50 | })); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | static GetFlags(fen) |
| 55 | { |
| 56 | let flags = [ |
| 57 | ChessRules.GetFlags(fen), //castle |
| 58 | { |
| 59 | "w": new Array(8), //pawns can move 2 squares |
| 60 | "b": new Array(8) |
| 61 | } |
| 62 | ]; |
| 63 | const fenFlags = fen.split(" ")[1].substr(4); //skip first 4 digits, for castle |
| 64 | for (let c of ['w','b']) |
| 65 | { |
| 66 | for (let i=0; i<8; i++) |
| 67 | flags[1][c][i] = (fenFlags.charAt((c=='w'?0:8)+i) == '1'); |
| 68 | } |
| 69 | return flags; |
| 70 | } |
| 71 | |
| 72 | // can color1 take color2? |
| 73 | canTake(color1, color2) |
| 74 | { |
| 75 | // Checkered aren't captured |
| 76 | return color1 != color2 && color2 != 'c' && (color1 != 'c' || color2 != this.turn); |
| 77 | } |
| 78 | |
| 79 | // Build regular move(s) from its initial and destination squares; tr: transformation |
| 80 | getBasicMove(sx, sy, ex, ey, tr) |
| 81 | { |
| 82 | if (this.board[ex][ey] == VariantRules.EMPTY) |
| 83 | { |
| 84 | // No capture, standard move construction |
| 85 | return [super.getBasicMove(sx,sy,ex,ey,tr)]; |
| 86 | } |
| 87 | let moves = []; //captures: generally 2 choices, unless 'tr' is specified or piece==king |
| 88 | const startPiece = this.getPiece(sx,sy); |
| 89 | const endPiece = this.getPiece(ex,ey); |
| 90 | const startColor = this.getColor(sx,sy); |
| 91 | const endColor = this.getColor(ex,ey); |
| 92 | for (let piece of !!tr ? [tr] : |
| 93 | (startPiece==VariantRules.KING ? VariantRules.KING : _.uniq([startPiece,endPiece]))) |
| 94 | { |
| 95 | var mv = new Move({ |
| 96 | appear: [ |
| 97 | new PiPo({ |
| 98 | x: ex, |
| 99 | y: ey, |
| 100 | c: startPiece==VariantRules.KING ? startColor : 'c', |
| 101 | p: piece |
| 102 | }) |
| 103 | ], |
| 104 | vanish: [ |
| 105 | new PiPo({ |
| 106 | x: sx, |
| 107 | y: sy, |
| 108 | c: startColor, |
| 109 | p: startPiece |
| 110 | }), |
| 111 | new PiPo({ |
| 112 | x: ex, |
| 113 | y: ey, |
| 114 | c: endColor, |
| 115 | p: endPiece |
| 116 | }) |
| 117 | ] |
| 118 | }); |
| 119 | moves.push(mv); |
| 120 | } |
| 121 | return moves; |
| 122 | } |
| 123 | |
| 124 | // Generic method to find possible moves of non-pawn pieces ("sliding or jumping") |
| 125 | getSlideNJumpMoves(x, y, color, steps, oneStep) |
| 126 | { |
| 127 | var moves = []; |
| 128 | let [sizeX,sizeY] = VariantRules.size; |
| 129 | outerLoop: |
| 130 | for (var loop=0; loop<steps.length; loop++) |
| 131 | { |
| 132 | var step = steps[loop]; |
| 133 | var i = x + step[0]; |
| 134 | var j = y + step[1]; |
| 135 | while (i>=0 && i<sizeX && j>=0 && j<sizeY |
| 136 | && this.board[i][j] == VariantRules.EMPTY) |
| 137 | { |
| 138 | moves.push(this.getBasicMove(x, y, i, j)[0]); //no capture |
| 139 | if (oneStep !== undefined) |
| 140 | continue outerLoop; |
| 141 | i += step[0]; |
| 142 | j += step[1]; |
| 143 | } |
| 144 | if (i>=0 && i<8 && j>=0 && j<8 && this.canTake(color, this.getColor(i,j))) |
| 145 | moves = moves.concat(this.getBasicMove(x, y, i, j)); |
| 146 | } |
| 147 | return moves; |
| 148 | } |
| 149 | |
| 150 | // What are the pawn moves from square x,y considering color "color" ? |
| 151 | getPotentialPawnMoves(x, y, color) |
| 152 | { |
| 153 | var moves = []; |
| 154 | var V = VariantRules; |
| 155 | let [sizeX,sizeY] = VariantRules.size; |
| 156 | const c = (color == 'c' ? this.turn : color); |
| 157 | const shift = (c == "w" ? -1 : 1); |
| 158 | let startRank = (c == "w" ? sizeY-2 : 1); |
| 159 | let lastRank = (c == "w" ? 0 : sizeY-1); |
| 160 | |
| 161 | if (x+shift >= 0 && x+shift < sizeX && x+shift != lastRank) |
| 162 | { |
| 163 | // Normal moves |
| 164 | if (this.board[x+shift][y] == V.EMPTY) |
| 165 | { |
| 166 | moves.push(this.getBasicMove(x, y, x+shift, y)[0]); |
| 167 | if (x==startRank && this.board[x+2*shift][y] == V.EMPTY && this.flags[1][c][y]) |
| 168 | { |
| 169 | // Two squares jump |
| 170 | moves.push(this.getBasicMove(x, y, x+2*shift, y)[0]); |
| 171 | } |
| 172 | } |
| 173 | // Captures |
| 174 | if (y>0 && this.canTake(this.getColor(x,y), this.getColor(x+shift,y-1)) |
| 175 | && this.board[x+shift][y-1] != V.EMPTY) |
| 176 | { |
| 177 | moves = moves.concat(this.getBasicMove(x, y, x+shift, y-1)); |
| 178 | } |
| 179 | if (y<sizeY-1 && this.canTake(this.getColor(x,y), this.getColor(x+shift,y+1)) |
| 180 | && this.board[x+shift][y+1] != V.EMPTY) |
| 181 | { |
| 182 | moves = moves.concat(this.getBasicMove(x, y, x+shift, y+1)); |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | if (x+shift == lastRank) |
| 187 | { |
| 188 | // Promotion |
| 189 | let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]; |
| 190 | promotionPieces.forEach(p => { |
| 191 | // Normal move |
| 192 | if (this.board[x+shift][y] == V.EMPTY) |
| 193 | moves.push(this.getBasicMove(x, y, x+shift, y, p)[0]); |
| 194 | // Captures |
| 195 | if (y>0 && this.canTake(this.getColor(x,y), this.getColor(x+shift,y-1)) |
| 196 | && this.board[x+shift][y-1] != V.EMPTY) |
| 197 | { |
| 198 | moves = moves.concat(this.getBasicMove(x, y, x+shift, y-1, p)); |
| 199 | } |
| 200 | if (y<sizeY-1 && this.canTake(this.getColor(x,y), this.getColor(x+shift,y+1)) |
| 201 | && this.board[x+shift][y+1] != V.EMPTY) |
| 202 | { |
| 203 | moves = moves.concat(this.getBasicMove(x, y, x+shift, y+1, p)); |
| 204 | } |
| 205 | }); |
| 206 | } |
| 207 | |
| 208 | // En passant |
| 209 | const Lep = this.epSquares.length; |
| 210 | const epSquare = Lep>0 ? this.epSquares[Lep-1] : undefined; |
| 211 | if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1) |
| 212 | { |
| 213 | let epStep = epSquare.y - y; |
| 214 | var enpassantMove = this.getBasicMove(x, y, x+shift, y+epStep)[0]; |
| 215 | enpassantMove.vanish.push({ |
| 216 | x: x, |
| 217 | y: y+epStep, |
| 218 | p: 'p', |
| 219 | c: this.getColor(x,y+epStep) |
| 220 | }); |
| 221 | enpassantMove.appear[0].c = 'c'; |
| 222 | moves.push(enpassantMove); |
| 223 | } |
| 224 | |
| 225 | return moves; |
| 226 | } |
| 227 | |
| 228 | getCastleMoves(x,y,c) |
| 229 | { |
| 230 | if (x != (c=="w" ? 7 : 0) || y != this.INIT_COL_KING[c]) |
| 231 | return []; //x isn't first rank, or king has moved (shortcut) |
| 232 | |
| 233 | const V = VariantRules; |
| 234 | |
| 235 | // Castling ? |
| 236 | const oppCol = this.getOppCol(c); |
| 237 | let moves = []; |
| 238 | let i = 0; |
| 239 | const finalSquares = [ [2,3], [6,5] ]; //king, then rook |
| 240 | castlingCheck: |
| 241 | for (let castleSide=0; castleSide < 2; castleSide++) //large, then small |
| 242 | { |
| 243 | if (!this.flags[0][c][castleSide]) |
| 244 | continue; |
| 245 | // If this code is reached, rooks and king are on initial position |
| 246 | |
| 247 | // Nothing on the path of the king (and no checks; OK also if y==finalSquare)? |
| 248 | let step = finalSquares[castleSide][0] < y ? -1 : 1; |
| 249 | for (i=y; i!=finalSquares[castleSide][0]; i+=step) |
| 250 | { |
| 251 | if (this.isAttacked([x,i], oppCol) || (this.board[x][i] != V.EMPTY && |
| 252 | // NOTE: next check is enough, because of chessboard constraints |
| 253 | (this.getColor(x,i) != c || ![V.KING,V.ROOK].includes(this.getPiece(x,i))))) |
| 254 | { |
| 255 | continue castlingCheck; |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | // Nothing on the path to the rook? |
| 260 | step = castleSide == 0 ? -1 : 1; |
| 261 | for (i = y + step; i != this.INIT_COL_ROOK[c][castleSide]; i += step) |
| 262 | { |
| 263 | if (this.board[x][i] != V.EMPTY) |
| 264 | continue castlingCheck; |
| 265 | } |
| 266 | const rookPos = this.INIT_COL_ROOK[c][castleSide]; |
| 267 | |
| 268 | // Nothing on final squares, except maybe king and castling rook? |
| 269 | for (i=0; i<2; i++) |
| 270 | { |
| 271 | if (this.board[x][finalSquares[castleSide][i]] != V.EMPTY && |
| 272 | this.getPiece(x,finalSquares[castleSide][i]) != V.KING && |
| 273 | finalSquares[castleSide][i] != rookPos) |
| 274 | { |
| 275 | continue castlingCheck; |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | // If this code is reached, castle is valid |
| 280 | moves.push( new Move({ |
| 281 | appear: [ |
| 282 | new PiPo({x:x,y:finalSquares[castleSide][0],p:V.KING,c:c}), |
| 283 | new PiPo({x:x,y:finalSquares[castleSide][1],p:V.ROOK,c:c})], |
| 284 | vanish: [ |
| 285 | new PiPo({x:x,y:y,p:V.KING,c:c}), |
| 286 | new PiPo({x:x,y:rookPos,p:V.ROOK,c:c})], |
| 287 | end: Math.abs(y - rookPos) <= 2 |
| 288 | ? {x:x, y:rookPos} |
| 289 | : {x:x, y:y + 2 * (castleSide==0 ? -1 : 1)} |
| 290 | }) ); |
| 291 | } |
| 292 | |
| 293 | return moves; |
| 294 | } |
| 295 | |
| 296 | canIplay(color, sq) |
| 297 | { |
| 298 | return ((color=='w' && this.movesCount%2==0) || color=='c' |
| 299 | || (color=='b' && this.movesCount%2==1)) |
| 300 | && [color,'c'].includes(this.getColor(sq[0], sq[1])); |
| 301 | } |
| 302 | |
| 303 | // Does m2 un-do m1 ? (to disallow undoing checkered moves) |
| 304 | oppositeMoves(m1, m2) |
| 305 | { |
| 306 | return m1.appear.length == 1 && m2.appear.length == 1 |
| 307 | && m1.vanish.length == 1 && m2.vanish.length == 1 |
| 308 | // && _.isEqual(m1.appear[0], m2.vanish[0]) //fails in HH case |
| 309 | // && _.isEqual(m1.vanish[0], m2.appear[0]); |
| 310 | && m1.start.x == m2.end.x && m1.end.x == m2.start.x |
| 311 | && m1.start.y == m2.end.y && m1.end.y == m2.start.y |
| 312 | && m1.appear[0].c == m2.vanish[0].c && m1.appear[0].p == m2.vanish[0].p |
| 313 | && m1.vanish[0].c == m2.appear[0].c && m1.vanish[0].p == m2.appear[0].p; |
| 314 | } |
| 315 | |
| 316 | filterValid(moves) |
| 317 | { |
| 318 | if (moves.length == 0) |
| 319 | return []; |
| 320 | let color = this.getColor( moves[0].start.x, moves[0].start.y ); |
| 321 | return moves.filter(m => { |
| 322 | const L = this.moves.length; |
| 323 | if (L > 0 && this.oppositeMoves(this.moves[L-1], m)) |
| 324 | return false; |
| 325 | return !this.underCheck(m, color); |
| 326 | }); |
| 327 | } |
| 328 | |
| 329 | isAttackedByPawn([x,y], c) |
| 330 | { |
| 331 | const color = (c=="c" ? this.turn : c); |
| 332 | let pawnShift = (color=="w" ? 1 : -1); |
| 333 | if (x+pawnShift>=0 && x+pawnShift<8) |
| 334 | { |
| 335 | for (let i of [-1,1]) |
| 336 | { |
| 337 | if (y+i>=0 && y+i<8 && this.getPiece(x+pawnShift,y+i)==VariantRules.PAWN |
| 338 | && this.getColor(x+pawnShift,y+i)==c) |
| 339 | { |
| 340 | return true; |
| 341 | } |
| 342 | } |
| 343 | } |
| 344 | return false; |
| 345 | } |
| 346 | |
| 347 | underCheck(move, c) |
| 348 | { |
| 349 | const color = c == 'c' ? this.turn : c; |
| 350 | this.play(move); |
| 351 | let res = this.isAttacked(this.kingPos[color], this.getOppCol(color)) |
| 352 | || this.isAttacked(this.kingPos[color], 'c'); //TODO: quite inefficient... |
| 353 | this.undo(move); |
| 354 | return res; |
| 355 | } |
| 356 | |
| 357 | getCheckSquares(move, c) |
| 358 | { |
| 359 | this.play(move); |
| 360 | const kingAttacked = this.isAttacked(this.kingPos[c], this.getOppCol(c)) |
| 361 | || this.isAttacked(this.kingPos[c], 'c'); |
| 362 | let res = kingAttacked |
| 363 | ? [ JSON.parse(JSON.stringify(this.kingPos[c])) ] //need to duplicate! |
| 364 | : [ ]; |
| 365 | this.undo(move); |
| 366 | return res; |
| 367 | } |
| 368 | |
| 369 | updateVariables(move) |
| 370 | { |
| 371 | const piece = this.getPiece(move.start.x,move.start.y); |
| 372 | const c = this.getColor(move.start.x,move.start.y); |
| 373 | |
| 374 | if (c != 'c') //checkered not concerned by castle flags |
| 375 | { |
| 376 | const firstRank = (c == "w" ? 7 : 0); |
| 377 | // Update king position + flags |
| 378 | if (piece == VariantRules.KING && move.appear.length > 0) |
| 379 | { |
| 380 | this.kingPos[c][0] = move.appear[0].x; |
| 381 | this.kingPos[c][1] = move.appear[0].y; |
| 382 | this.flags[0][c] = [false,false]; |
| 383 | return; |
| 384 | } |
| 385 | const oppCol = this.getOppCol(c); |
| 386 | const oppFirstRank = 7 - firstRank; |
| 387 | if (move.start.x == firstRank //our rook moves? |
| 388 | && this.INIT_COL_ROOK[c].includes(move.start.y)) |
| 389 | { |
| 390 | const flagIdx = move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1; |
| 391 | this.flags[0][c][flagIdx] = false; |
| 392 | } |
| 393 | else if (move.end.x == oppFirstRank //we took opponent rook? |
| 394 | && this.INIT_COL_ROOK[c].includes(move.end.y)) |
| 395 | { |
| 396 | const flagIdx = move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1; |
| 397 | this.flags[0][oppCol][flagIdx] = false; |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | // Does it turn off a 2-squares pawn flag? |
| 402 | const secondRank = [1,6]; |
| 403 | if (secondRank.includes(move.start.x) && move.vanish[0].p == VariantRules.PAWN) |
| 404 | this.flags[1][move.start.x==6 ? "w" : "b"][move.start.y] = false; |
| 405 | } |
| 406 | |
| 407 | play(move, ingame) |
| 408 | { |
| 409 | super.play(move, ingame); |
| 410 | if (!ingame) |
| 411 | this.moves.push(move); //needed for turn indication for checkered pieces |
| 412 | } |
| 413 | |
| 414 | undo(move) |
| 415 | { |
| 416 | super.undo(move); |
| 417 | this.moves.pop(); |
| 418 | } |
| 419 | |
| 420 | checkGameEnd(color) |
| 421 | { |
| 422 | if (!this.isAttacked(this.kingPos[color], this.getOppCol(color)) |
| 423 | && !this.isAttacked(this.kingPos[color], 'c')) |
| 424 | { |
| 425 | return "1/2"; |
| 426 | } |
| 427 | // OK, checkmate |
| 428 | return color == "w" ? "0-1" : "1-0"; |
| 429 | } |
| 430 | |
| 431 | evalPosition() |
| 432 | { |
| 433 | const [sizeX,sizeY] = VariantRules.size; |
| 434 | let evaluation = 0; |
| 435 | //Just count material for now, considering checkered neutral (...) |
| 436 | for (let i=0; i<sizeX; i++) |
| 437 | { |
| 438 | for (let j=0; j<sizeY; j++) |
| 439 | { |
| 440 | if (this.board[i][j] != VariantRules.EMPTY) |
| 441 | { |
| 442 | const sqColor = this.getColor(i,j); |
| 443 | const sign = sqColor == "w" ? 1 : (sqColor=="b" ? -1 : 0); |
| 444 | evaluation += sign * VariantRules.VALUES[this.getPiece(i,j)]; |
| 445 | } |
| 446 | } |
| 447 | } |
| 448 | return evaluation; |
| 449 | } |
| 450 | |
| 451 | static GenRandInitFen() |
| 452 | { |
| 453 | let fen = ChessRules.GenRandInitFen(); |
| 454 | return fen.replace(/ - 0$/, "1111111111111111 - 0 -"); |
| 455 | } |
| 456 | |
| 457 | getFen() |
| 458 | { |
| 459 | let fen = super.getFen() + " "; |
| 460 | const L = this.moves.length; |
| 461 | if (L > 0 && this.moves[L-1].vanish.length==1 && this.moves[L-1].appear[0].c=="c") |
| 462 | { |
| 463 | // Ok, un-cancellable checkered move |
| 464 | fen += this.moves[L-1].appear[0].p |
| 465 | + this.moves[L-1].start.x + "," + this.moves[L-1].start.y + ";" |
| 466 | + this.moves[L-1].end.x + "," + this.moves[L-1].end.y; |
| 467 | } |
| 468 | else fen += "-"; |
| 469 | return fen; |
| 470 | } |
| 471 | |
| 472 | getFlagsFen() |
| 473 | { |
| 474 | let fen = ""; |
| 475 | // Add castling flags |
| 476 | for (let c of ['w','b']) |
| 477 | { |
| 478 | for (let i=0; i<2; i++) |
| 479 | fen += this.flags[0][c][i] ? '1' : '0'; |
| 480 | } |
| 481 | // Add pawns flags |
| 482 | for (let c of ['w','b']) |
| 483 | { |
| 484 | for (let i=0; i<8; i++) |
| 485 | fen += this.flags[1][c][i] ? '1' : '0'; |
| 486 | } |
| 487 | return fen; |
| 488 | } |
| 489 | |
| 490 | getNotation(move) |
| 491 | { |
| 492 | if (move.appear.length == 2) |
| 493 | { |
| 494 | // Castle |
| 495 | if (move.end.y < move.start.y) |
| 496 | return "0-0-0"; |
| 497 | else |
| 498 | return "0-0"; |
| 499 | } |
| 500 | |
| 501 | // Translate final square |
| 502 | let finalSquare = |
| 503 | String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x); |
| 504 | |
| 505 | let piece = this.getPiece(move.start.x, move.start.y); |
| 506 | if (piece == VariantRules.PAWN) |
| 507 | { |
| 508 | // Pawn move |
| 509 | let notation = ""; |
| 510 | if (move.vanish.length > 1) |
| 511 | { |
| 512 | // Capture |
| 513 | let startColumn = String.fromCharCode(97 + move.start.y); |
| 514 | notation = startColumn + "x" + finalSquare + "=" + move.appear[0].p.toUpperCase(); |
| 515 | } |
| 516 | else //no capture |
| 517 | notation = finalSquare; |
| 518 | if (move.appear.length > 0 && piece != move.appear[0].p) //promotion |
| 519 | notation += "=" + move.appear[0].p.toUpperCase(); |
| 520 | return notation; |
| 521 | } |
| 522 | |
| 523 | else |
| 524 | { |
| 525 | // Piece movement |
| 526 | return piece.toUpperCase() + (move.vanish.length > 1 ? "x" : "") + finalSquare |
| 527 | + (move.vanish.length > 1 ? "=" + move.appear[0].p.toUpperCase() : ""); |
| 528 | } |
| 529 | } |
| 530 | } |