templates.chess
1# Chess 2# Example for ILLUSTRATIVE PURPOSE ONLY 3 4import smartpy as sp 5 6 7@sp.module 8def main(): 9 def matrix(vms): 10 mm = {} 11 km = 0 12 for vvs in vms: 13 mv = {} 14 kv = 0 15 for vv in vvs: 16 mv[kv] = vv 17 kv += 1 18 mm[km] = mv 19 km += 1 20 return mm 21 22 PAWN = 1 23 ROOK = 2 24 KNIGHT = 3 25 BISHOP = 4 26 QUEEN = 5 27 KING = 6 28 29 N_PAWN = sp.nat(1) 30 N_ROOK = sp.nat(2) 31 N_KNIGHT = sp.nat(3) 32 N_BISHOP = sp.nat(4) 33 N_QUEEN = sp.nat(5) 34 N_KING = sp.nat(6) 35 36 t_status: type = sp.variant( 37 play=sp.unit, 38 force_play=sp.unit, 39 finished=sp.string, 40 claim_stalemate=sp.unit, 41 ) 42 43 t_move: type = sp.record( 44 f=sp.record(i=sp.int, j=sp.int), 45 t=sp.record(i=sp.int, j=sp.int), 46 promotion=sp.option[sp.nat], 47 claim_repeat=sp.option[ 48 sp.pair[sp.nat, sp.nat] 49 ], # Claim repeat after the move has been done, params: 2 other identical fullMove number 50 ) 51 52 t_board_state: type = sp.record( 53 castle=sp.map[sp.int, sp.map[sp.int, sp.bool]], 54 check=sp.bool, 55 deck=sp.map[sp.int, sp.map[sp.int, sp.int]], 56 enPassant=sp.option[sp.record(i=sp.int, j=sp.int)], 57 fullMove=sp.nat, 58 halfMoveClock=sp.nat, 59 kings=sp.map[sp.int, sp.record(i=sp.int, j=sp.int)], 60 nextPlayer=sp.int, 61 # pastMoves is (fullMove, player): blake2b(pack(castle, deck, enPassant)) 62 pastMoves=sp.map[sp.pair[sp.nat, sp.int], sp.bytes], 63 ) 64 65 t_get_movable_to_params: type = sp.record( 66 i=sp.int, 67 j=sp.int, 68 player=sp.int, 69 deck=sp.map[sp.int, sp.map[sp.int, sp.int]], 70 pawn_capturing=sp.bool, 71 ) 72 73 def sign(x): 74 return 1 if x > 0 else (-1 if x < 0 else 0) 75 76 def verify_repeat(param): 77 """Verify if the same position has been observed 3 times. 78 79 Two positions are by definition "the same" if: 80 - the same types of pieces occupy the same squares 81 - the same player has the move 82 - the remaining castling rights are the same and the possibility to capture "en passant" is the same. 83 The repeated positions not need to occur in succession. 84 85 FullMoves start at 1 and is incremented only after both players_map played one time. 86 """ 87 (board_state_, player, fullMoves) = param 88 sp.cast(fullMoves, sp.set[sp.nat]) 89 assert sp.len(fullMoves) == 3 90 for fullMove in fullMoves.elements(): 91 assert ( 92 sp.blake2b( 93 sp.pack( 94 ( 95 board_state_.castle, 96 board_state_.deck, 97 board_state_.enPassant, 98 ) 99 ) 100 ) 101 == board_state_.pastMoves[(fullMove, player)] 102 ), ("NotSameMove", sp.record(fullMove=fullMove)) 103 104 class Chess(sp.Contract): 105 def __init__(self, player1, player2): 106 self.data.board_state = sp.cast( 107 sp.record( 108 castle=({1: {-1: True, 1: True}, -1: {-1: True, 1: True}}), 109 check=False, 110 deck=sp.cast( 111 matrix( 112 [ 113 [2, 3, 4, 5, 6, 4, 3, 2], 114 [1, 1, 1, 1, 1, 1, 1, 1], 115 [0, 0, 0, 0, 0, 0, 0, 0], 116 [0, 0, 0, 0, 0, 0, 0, 0], 117 [0, 0, 0, 0, 0, 0, 0, 0], 118 [0, 0, 0, 0, 0, 0, 0, 0], 119 [-1, -1, -1, -1, -1, -1, -1, -1], 120 [-2, -3, -4, -5, -6, -4, -3, -2], 121 ] 122 ), 123 sp.map[sp.int, sp.map[sp.int, sp.int]], 124 ), 125 enPassant=None, 126 fullMove=sp.nat(1), 127 halfMoveClock=sp.nat(0), 128 kings={-1: sp.record(i=7, j=4), 1: sp.record(i=0, j=4)}, 129 nextPlayer=1, 130 pastMoves={ 131 (0, -1): sp.bytes( 132 "0xbaf82e0f3ca2b90e482d6b6846873df9fee4e1baf369bc897bcef3b113a359df" 133 ) 134 }, 135 ), 136 t_board_state, 137 ) 138 self.data.draw_offer = set() 139 self.data.metadata = sp.big_map() 140 self.data.players = {player1, player2} 141 self.data.players_map = {-1: player2, 1: player1} 142 self.data.status = sp.variant.play() 143 144 sp.cast( 145 self.data, 146 sp.record( 147 board_state=t_board_state, 148 draw_offer=sp.set[sp.address], 149 metadata=sp.big_map[sp.string, sp.bytes], 150 players=sp.set[sp.address], 151 players_map=sp.map[sp.int, sp.address], 152 status=t_status, 153 ), 154 ) 155 156 # list_of_views = [self.build_fen] 157 # metadata_base = { 158 # "version": "alpha 0.1", 159 # "description" : ( 160 # """Example of a chess game contract.\n 161 # Given as an ILLUSTRATIVE PURPOSE ONLY 162 # """ 163 # ), 164 # "interfaces": ["TZIP-016"], 165 # "authors": [ 166 # "SmartPy <https://smartpy.io>" 167 # ], 168 # "homepage": "https://smartpy.io", 169 # "views": list_of_views, 170 # "source": { 171 # "tools": ["SmartPy"], 172 # "location": "https://gitlab.com/SmartPy/smartpy/-/blob/master/python/templates/state_channel_games/game_platform.py" 173 # }, 174 # } 175 # self.init_metadata("metadata_base", metadata_base) 176 177 # Offchain views # 178 179 @sp.offchain_view 180 def build_fen(self): 181 """Return the state of the board chess in the fen standard""" 182 fen = "" 183 rep = { 184 1: "P", 185 2: "R", 186 3: "N", 187 4: "B", 188 5: "Q", 189 6: "K", 190 -1: "p", 191 -2: "r", 192 -3: "n", 193 -4: "b", 194 -5: "q", 195 -6: "k", 196 } 197 column = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 7: "h"} 198 to_str = { 199 0: "0", 200 1: "1", 201 2: "2", 202 3: "3", 203 4: "4", 204 5: "5", 205 6: "6", 206 7: "7", 207 8: "8", 208 9: "9", 209 } 210 empty = 0 211 nb_lines = 0 212 213 # Pieces 214 for row in reversed(self.data.board_state.deck.values()): 215 for piece in row.values(): 216 if piece == 0: 217 empty += 1 218 else: 219 if empty > 0: 220 fen += to_str[empty] 221 empty = 0 222 223 fen += rep[piece] 224 225 if empty > 0: 226 fen += to_str[empty] 227 empty = 0 228 229 nb_lines += 1 230 if nb_lines < 8: 231 fen += "/" 232 233 # Next player 234 if self.data.board_state.nextPlayer == 1: 235 fen += " w " 236 else: 237 fen += " b " 238 239 # Castling 240 if self.data.board_state.castle[1][1]: 241 fen += "K" 242 if self.data.board_state.castle[1][-1]: 243 fen += "Q" 244 if self.data.board_state.castle[-1][1]: 245 fen += "k" 246 if self.data.board_state.castle[-1][-1]: 247 fen += "q" 248 if not ( 249 self.data.board_state.castle[1][1] 250 or self.data.board_state.castle[1][-1] 251 or self.data.board_state.castle[-1][1] 252 or self.data.board_state.castle[-1][-1] 253 ): 254 fen += "-" 255 256 # En passant 257 if self.data.board_state.enPassant.is_some(): 258 enPassant = self.data.board_state.enPassant.unwrap_some() 259 fen += " " + column[enPassant.j] 260 fen += to_str[sp.as_nat(enPassant.i + 1)] + " " 261 else: 262 fen += " - " 263 264 # Halfmove clock 265 (q, r) = sp.ediv(self.data.board_state.halfMoveClock, 10).unwrap_some() 266 if q > 0: 267 fen += to_str[q] 268 fen += to_str[r] + " " 269 270 # Fullmove number 271 (q2, r2) = sp.ediv(self.data.board_state.fullMove, 10).unwrap_some() 272 if q2 > 0: 273 (q3, r3) = sp.ediv(q2, 10).unwrap_some() 274 if q3 > 0: 275 fen += to_str[q3] 276 fen += to_str[r3] 277 fen += to_str[r2] 278 279 return fen 280 281 # Private lambdas # 282 283 @sp.private 284 def get_movable_to(self, i, j, player, deck, pawn_capturing): 285 """Return player's list of pieces that are capable to move on a square. 286 ASSUME That the square doesn't contain a piece owned by `player`. 287 ASSUME That the square contains a piece owned by opponent if `pawn_capturing` is True 288 Don't take into account "En passant". 289 Don't take into account if the move/take is illegal because player would be in check after it. 290 291 Params: 292 i, j (sp.int): square's coordinates 293 player (sp.int): attacking player 294 deck: current deck 295 pawn_capturing (sp.bool): if pawn_capturing is True: pawns are capturing else: moving 296 """ 297 sp.cast(i, sp.int) 298 sp.cast(j, sp.int) 299 sp.cast(player, sp.int) 300 sp.cast(deck, sp.map[sp.int, sp.map[sp.int, sp.int]]) 301 sp.cast(pawn_capturing, sp.bool) 302 303 check = sp.cast([], sp.list[sp.record(i=sp.int, j=sp.int)]) 304 305 # square attacked by a pawn 306 if pawn_capturing: 307 for c in [(i + player, j - 1), (i + player, j + 1)]: 308 if ( 309 deck.get(sp.fst(c), default={}).get(sp.snd(c), default=0) 310 == PAWN * player 311 ): 312 check.push(sp.record(i=sp.fst(c), j=sp.snd(c))) 313 314 # Pawn can move onto it 315 else: 316 # move by 1 317 c = (i - player, j) 318 if deck.get(sp.fst(c), default={}).get(sp.snd(c), default=0) == ( 319 PAWN * player 320 ): 321 check.push(sp.record(i=sp.fst(c), j=sp.snd(c))) 322 323 # jump 324 if (player == 1 and i == 3) or (player == -1 and i == 4): 325 c = (i + player * 2, j) 326 if deck.get(sp.fst(c), default={}).get(sp.snd(c), default=0) == ( 327 PAWN * player 328 ): 329 check.push(sp.record(i=sp.fst(c), j=sp.snd(c))) 330 331 # square attacked by a knight 332 for c in [ 333 (i + 2, j - 1), 334 (i + 2, j + 1), 335 (i + 1, j - 2), 336 (i + 1, j + 2), 337 (i - 2, j - 1), 338 (i - 2, j + 1), 339 (i - 1, j - 2), 340 (i - 1, j + 2), 341 ]: 342 if ( 343 deck.get(sp.fst(c), default={}).get(sp.snd(c), default=0) 344 == KNIGHT * player 345 ): 346 check.push(sp.record(i=sp.fst(c), j=sp.snd(c))) 347 348 # square attacked by a king 349 # only verified if the i j square doesn't correspond to player's KING 350 if deck.get(i, default={}).get(j, default=0) != (KING * player * -1): 351 for c in [ 352 (i + 1, j + 1), 353 (i - 1, j - 1), 354 (i + 1, j - 1), 355 (i - 1, j + 1), 356 (i + 0, j + 1), 357 (i + 1, j + 0), 358 (i + 0, j - 1), 359 (i - 1, j + 0), 360 ]: 361 if ( 362 deck.get(sp.fst(c), default={}).get(sp.snd(c), default=0) 363 == KING * player 364 ): 365 check.push(sp.record(i=sp.fst(c), j=sp.snd(c))) 366 367 @sp.effects 368 def check_queen_other(param): 369 (i_c, j_c, other, continue_, c_deck, player_, check) = param 370 check_ = check 371 new_continue = continue_ 372 if new_continue: 373 c = c_deck.get(i_c, default={}).get(j_c, default=0) 374 if (c != 0) and (c != KING * player_ * -1): 375 if c == QUEEN * player_ or c == other * player_: 376 check_.push(sp.record(i=i_c, j=j_c)) 377 else: 378 new_continue = False 379 return (check_, new_continue) 380 381 continue_ = True 382 383 # line/column attacked by a rook/queen 384 ranges = {0: j, 1: 8 - j, 2: i, 3: 8 - i} 385 k = [(0, -1), (0, 1), (-1, 0), (1, 0)] 386 n = 0 387 for x in k: 388 # TODO: replace when unpair in for is possible 389 (a, b) = x 390 for l in range(1, ranges[n]): 391 (new_check, new_continue) = check_queen_other( 392 (i + l * a, j + l * b, ROOK, continue_, deck, player, check) 393 ) 394 check = new_check 395 continue_ = new_continue 396 continue_ = True 397 n += 1 398 399 # diagonals attacked by a bishop/queen 400 ranges = { 401 0: sp.min(i, j), 402 1: sp.min(8 - i, 8 - j), 403 2: sp.min(i, 8 - j), 404 3: sp.min(8 - i, j), 405 } 406 k = [(-1, -1), (1, 1), (-1, 1), (1, -1)] 407 n = 0 408 for x in k: 409 # TODO: replace when unpair in for is possible 410 (a, b) = x 411 for l in range(1, ranges[n]): 412 assert j + l * b < 9, sp.record(i=i, j=j, l=l, b=b, n=n) 413 (new_check, new_continue) = check_queen_other( 414 (i + l * a, j + l * b, BISHOP, continue_, deck, player, check) 415 ) 416 check = new_check 417 continue_ = new_continue 418 continue_ = True 419 n += 1 420 421 return sp.cast(check, sp.list[sp.record(i=sp.int, j=sp.int)]) 422 423 @sp.private 424 def move_piece(self, board_state, move, get_movable_to): 425 """ 426 Move a piece from one square to another in the deck 427 Change the board_state accordingly (deck, kings, check, castle, enPassant, halfMoveClock, fullMove) 428 Return a TBool to True if the game is draw 429 Do not detect checkmate 430 Return (t_board_state, TBool): new board state and `is_draw` 431 432 """ 433 is_draw = False 434 new_board_state = board_state 435 436 assert move.f.i >= 0 and move.f.i < 8 # from position 437 assert move.f.j >= 0 and move.f.j < 8 438 assert move.t.i >= 0 and move.t.i < 8 # to position 439 assert move.t.j >= 0 and move.t.j < 8 440 deltaY = move.t.i - move.f.i 441 deltaX = move.t.j - move.f.j 442 assert deltaX != 0 or deltaY != 0 443 nextPlayer = new_board_state.nextPlayer 444 deck = new_board_state.deck 445 # Moving its own piece 446 assert sign(deck[move.f.i][move.f.j]) == nextPlayer 447 # Not moving onto its own piece 448 assert sign(deck[move.t.i][move.t.j]) != nextPlayer 449 # Not moving onto a king 450 assert deck[move.t.i][move.t.j] != KING * nextPlayer 451 452 resetClock = False 453 promotion = False 454 455 ########### 456 # King move 457 if abs(deck[move.f.i][move.f.j]) == N_KING: 458 # Castling 459 if abs(deltaX) == 2 and deltaY == 0: 460 assert (nextPlayer == 1 and move.f.i == 0 and move.f.j == 4) or ( 461 nextPlayer == -1 and move.f.i == 7 and move.f.j == 4 462 ) 463 assert new_board_state.castle[nextPlayer][sign(deltaX)] 464 if deltaX > 0: 465 deck[move.t.i][7] = 0 466 else: 467 deck[move.t.i][0] = 0 468 deck[move.t.i][move.t.j - sign(deltaX)] = ROOK * nextPlayer 469 470 # Standard move 471 else: 472 assert abs(deltaX) <= 1 and abs(deltaY) <= 1 473 474 new_board_state.kings[nextPlayer] = sp.record(i=move.t.i, j=move.t.j) 475 new_board_state.castle[nextPlayer] = {-1: False, 1: False} 476 477 ########### 478 # Rook move 479 480 if abs(deck[move.f.i][move.f.j]) == N_ROOK: 481 assert deltaX == 0 or deltaY == 0 482 for k in range(1, sp.to_int(sp.max(abs(deltaX), abs(deltaY))) - 1): 483 dx = sign(deltaX) 484 dy = sign(deltaY) 485 assert deck[move.f.i + k * dy][move.f.j + k * dx] == 0 486 487 left_side = (nextPlayer == 1 and move.f.i == 0 and move.f.j == 0) or ( 488 nextPlayer == -1 and move.f.i == 7 and move.f.j == 0 489 ) 490 right_side = (nextPlayer == 1 and move.f.i == 0 and move.f.j == 7) or ( 491 nextPlayer == -1 and move.f.i == 7 and move.f.j == 7 492 ) 493 494 if left_side: 495 new_board_state.castle[nextPlayer][-1] = False 496 if right_side: 497 new_board_state.castle[nextPlayer][1] = False 498 499 ############ 500 # Queen move 501 502 if abs(deck[move.f.i][move.f.j]) == N_QUEEN: 503 assert deltaX == 0 or deltaY == 0 or abs(deltaX) == abs(deltaY) 504 for k in range(1, sp.to_int(sp.max(abs(deltaX), abs(deltaY))) - 1): 505 dx = sign(deltaX) 506 dy = sign(deltaY) 507 assert deck[move.f.i + k * dy][move.f.j + k * dx] == 0 508 509 ############# 510 # Bishop move 511 512 if abs(deck[move.f.i][move.f.j]) == N_BISHOP: 513 assert abs(deltaX) == abs(deltaY) 514 for k in range(1, sp.to_int(sp.max(abs(deltaX), abs(deltaY))) - 1): 515 dx = sign(deltaX) 516 dy = sign(deltaY) 517 assert deck[move.f.i + k * dy][move.f.j + k * dx] == 0 518 519 ############# 520 # Knight move 521 if abs(deck[move.f.i][move.f.j]) == N_KNIGHT: 522 assert abs(deltaX * deltaY) == 2 523 524 ########### 525 # Pawn move 526 527 # Pawn move must be after every other 528 # Because of the promotion system 529 if abs(deck[move.f.i][move.f.j]) == N_PAWN: 530 assert ( 531 # En passant 532 ( 533 abs(deltaX) == 1 534 and deltaY * nextPlayer == 1 535 and board_state.enPassant.is_some() 536 and move.f.i == board_state.enPassant.unwrap_some().i 537 and move.f.j == board_state.enPassant.unwrap_some().j 538 ) 539 # Jump 540 or ( 541 deltaX == 0 542 and deltaY * nextPlayer == 2 543 and deck[move.t.i - nextPlayer][move.t.j] == 0 544 ) 545 # Move 546 or (deltaX == 0 and deltaY * nextPlayer == 1) 547 # Take 548 or ( 549 abs(deltaX) == 1 550 and deltaY * nextPlayer == 1 551 and deck[move.t.i][move.t.j] != nextPlayer 552 ) 553 ) 554 555 if deltaY * nextPlayer == 2: 556 new_board_state.enPassant = sp.Some( 557 sp.record(i=move.t.i - nextPlayer, j=move.t.j) 558 ) 559 else: 560 new_board_state.enPassant = None 561 resetClock = True 562 # Promotion 563 if move.t.i == 7 or move.t.i == 0: 564 promotion = True 565 else: 566 new_board_state.enPassant = None 567 568 king = new_board_state.kings[nextPlayer] 569 570 if deck[move.t.i][move.t.j] != 0: 571 resetClock = True 572 573 if promotion: 574 piece = move.promotion.unwrap_some() 575 assert piece > 1 and piece < 6 576 deck[move.t.i][move.t.j] = sp.mul(nextPlayer, piece) 577 else: 578 assert move.promotion.is_none() 579 deck[move.t.i][move.t.j] = deck[move.f.i][move.f.j] 580 deck[move.f.i][move.f.j] = 0 581 582 # Not in check after the move 583 assert ( 584 sp.len( 585 get_movable_to( 586 sp.record( 587 i=king.i, 588 j=king.j, 589 player=nextPlayer * -1, 590 deck=deck, 591 pawn_capturing=True, 592 ) 593 ) 594 ) 595 == 0 596 ) 597 598 # Register move 599 new_board_state.pastMoves[ 600 (new_board_state.fullMove, nextPlayer) 601 ] = sp.blake2b( 602 sp.pack((new_board_state.castle, deck, new_board_state.enPassant)) 603 ) 604 new_board_state.deck = deck 605 606 if move.claim_repeat.is_some(): 607 (fullMove1, fullMove2) = move.claim_repeat.unwrap_some() 608 verify_repeat( 609 ( 610 new_board_state, 611 nextPlayer, 612 {fullMove1, fullMove2, new_board_state.fullMove}, 613 ) 614 ) 615 is_draw = True 616 617 nextPlayer *= -1 618 if nextPlayer == 1: 619 new_board_state.fullMove += 1 620 if resetClock: 621 new_board_state.halfMoveClock = 0 622 else: 623 new_board_state.halfMoveClock += 1 624 625 king = new_board_state.kings[nextPlayer] 626 627 if ( 628 sp.len( 629 get_movable_to( 630 sp.record( 631 i=king.i, 632 j=king.j, 633 player=nextPlayer * -1, 634 deck=deck, 635 pawn_capturing=True, 636 ) 637 ) 638 ) 639 > 0 640 ): 641 new_board_state.check = True 642 643 else: 644 new_board_state.check = False 645 646 if new_board_state.halfMoveClock > 49: 647 is_draw = True 648 649 return (new_board_state, is_draw) 650 651 @sp.private 652 def is_checkmate(self, board_state, get_movable_to): 653 """Return sp.bool(True) if the board_state represents a checkmate""" 654 655 def not_defending(param): 656 ( 657 defending_squares, 658 attacking_square, 659 deck, 660 initial_king, 661 nextPlayer, 662 checkmate, 663 get_movable_to_, 664 ) = param 665 """ Return True if none of the defending_squares can be moved to attacking_square to remove the king's check (without verifying move rules)""" 666 checkmate_ = checkmate 667 for defending_square in defending_squares: 668 if checkmate: 669 test_deck = deck 670 test_deck[attacking_square.i][attacking_square.j] = test_deck[ 671 defending_square.i 672 ][defending_square.j] 673 test_deck[defending_square.i][defending_square.j] = 0 674 king = initial_king 675 if ( 676 test_deck[attacking_square.i][attacking_square.j] 677 == KING * nextPlayer 678 ): 679 king = sp.record(i=attacking_square.i, j=attacking_square.j) 680 # Is not in check? 681 if ( 682 sp.len( 683 get_movable_to_( 684 sp.record( 685 i=king.i, 686 j=king.j, 687 player=nextPlayer * -1, 688 deck=test_deck, 689 pawn_capturing=True, 690 ) 691 ) 692 ) 693 == 0 694 ): 695 checkmate_ = False 696 return checkmate_ 697 698 checkmate = True 699 700 nextPlayer = board_state.nextPlayer 701 king = board_state.kings[nextPlayer] 702 deck = board_state.deck 703 704 # Are we in check? 705 attacking_squares = get_movable_to( 706 sp.record( 707 i=king.i, 708 j=king.j, 709 player=nextPlayer * -1, 710 deck=deck, 711 pawn_capturing=True, 712 ) 713 ) 714 if sp.len(attacking_squares) == 0: 715 checkmate = False 716 else: 717 # Can the King escape 718 for x in [ 719 (1, -1), 720 (1, 0), 721 (1, 1), 722 (0, 1), 723 (-1, 1), 724 (-1, 0), 725 (-1, -1), 726 (0, -1), 727 ]: 728 # TODO: replace when unpair in for is possible 729 (i, j) = x 730 if checkmate: 731 move_i = king.i + i 732 move_j = king.j + j 733 destination_piece = deck.get(move_i, default={}).get( 734 move_j, default=nextPlayer * PAWN 735 ) 736 # Is this destination valid? 737 if ( 738 destination_piece == 0 739 or sign(destination_piece) != nextPlayer 740 ): 741 # Would the king be in check if he moves to it? 742 if ( 743 sp.len( 744 get_movable_to( 745 sp.record( 746 i=move_i, 747 j=move_j, 748 player=nextPlayer * -1, 749 deck=deck, 750 pawn_capturing=True, 751 ) 752 ) 753 ) 754 == 0 755 ): 756 checkmate = False 757 758 if checkmate: 759 # Can we capture the attacking square or move a piece to block the attack? 760 if sp.len(attacking_squares) == 1: 761 # There is no move except escape to protect against 2 attacking pieces 762 for attacking_square in attacking_squares: 763 # Can we capture the attacking square? 764 defending_squares = get_movable_to( 765 sp.record( 766 i=attacking_square.i, 767 j=attacking_square.j, 768 player=nextPlayer * -1, 769 deck=deck, 770 pawn_capturing=True, 771 ) 772 ) 773 774 checkmate = not_defending( 775 ( 776 defending_squares, 777 attacking_square, 778 deck, 779 king, 780 nextPlayer, 781 checkmate, 782 get_movable_to, 783 ) 784 ) 785 786 if checkmate: 787 # Can we move a piece to block the attack? 788 # KNIGHT and PAWN cannot be blocked by moving a piece between them and the king 789 if ( 790 deck[attacking_square.i][attacking_square.j] 791 != KNIGHT * nextPlayer * -1 792 ) and ( 793 deck[attacking_square.i][attacking_square.j] 794 != PAWN * nextPlayer * -1 795 ): 796 if attacking_square.i == king.i: 797 # Can we put an obstructing piece in this column? 798 for obstructing_j in range( 799 sp.min(king.j, attacking_square.j), 800 sp.max(king.j, attacking_square.j), 801 ): 802 defending_squares = get_movable_to( 803 sp.record( 804 i=king.i, 805 j=obstructing_j, 806 player=nextPlayer, 807 deck=deck, 808 pawn_capturing=False, 809 ) 810 ) 811 checkmate = not_defending( 812 ( 813 defending_squares, 814 sp.record( 815 i=king.i, j=obstructing_j 816 ), 817 deck, 818 king, 819 nextPlayer, 820 checkmate, 821 get_movable_to, 822 ) 823 ) 824 825 else: 826 if attacking_square.j == king.j: 827 # Can we put an obstructing piece in this line? 828 for obstructing_i in range( 829 sp.min(king.i, attacking_square.i), 830 sp.max(king.i, attacking_square.i), 831 ): 832 defending_squares = get_movable_to( 833 sp.record( 834 i=obstructing_i, 835 j=king.j, 836 player=nextPlayer, 837 deck=deck, 838 pawn_capturing=False, 839 ) 840 ) 841 checkmate = not_defending( 842 ( 843 defending_squares, 844 sp.record( 845 i=obstructing_i, j=king.j 846 ), 847 deck, 848 king, 849 nextPlayer, 850 checkmate, 851 get_movable_to, 852 ) 853 ) 854 855 else: 856 # Can we put an obstructing piece in this diagonal? 857 d = sp.to_int( 858 abs(king.i - attacking_square.i) 859 ) 860 vec_i = sign(attacking_square.i - king.i) 861 vec_j = sign(attacking_square.j - king.j) 862 863 for i in range(1, d): 864 obstructing_i = king.i + vec_i * i 865 obstructing_j = king.j + vec_j * i 866 defending_squares = get_movable_to( 867 sp.record( 868 i=obstructing_i, 869 j=obstructing_j, 870 player=nextPlayer, 871 deck=deck, 872 pawn_capturing=False, 873 ) 874 ) 875 checkmate = not_defending( 876 ( 877 defending_squares, 878 sp.record( 879 i=obstructing_i, 880 j=obstructing_j, 881 ), 882 deck, 883 king, 884 nextPlayer, 885 checkmate, 886 get_movable_to, 887 ) 888 ) 889 return checkmate 890 891 # entrypoints # 892 893 @sp.entrypoint 894 def giveup(self): 895 """Giveup the game.""" 896 assert not self.data.status.is_variant.finished() 897 if sp.sender == self.data.players_map[-1]: 898 self.data.status = sp.variant.finished("player_1_won") 899 else: 900 if sp.sender == self.data.players_map[1]: 901 self.data.status = sp.variant.finished("player_2_won") 902 else: 903 raise "Wrong player" 904 905 @sp.entrypoint 906 def threefold_repetition_claim(self, fullMove1, fullMove2): 907 """Claim draw by 3 repeated moves by giving 2 fullMove numbers identical to the current move. 908 909 A player may claim a draw if the same position occured three times. 910 Two positions are by definition "the same" if: 911 - the same types of pieces occupy the same squares 912 - the same player has the move 913 - the remaining castling rights are the same and the possibility to capture en passant is the same. 914 The repeated positions need not occur in succession. 915 916 The `threefold_repetition_claim` entrypoint can only be called before playing a move. 917 Players can claim threefold repetition after having done a move by using the `play` entrypoint. 918 919 Fullmove start at 1 and is incremented only after both players_map played one time. 920 """ 921 assert not self.data.status.is_variant.finished(), "Game finished" 922 assert ( 923 self.data.players_map[self.data.board_state.nextPlayer] == sp.sender 924 ), "Wrong player" 925 previousFullMove = ( 926 sp.as_nat(self.data.board_state.fullMove - 1) 927 if self.data.board_state.nextPlayer == 1 928 else self.data.board_state.fullMove 929 ) 930 verify_repeat( 931 ( 932 self.data.board_state, 933 self.data.board_state.nextPlayer * -1, 934 {fullMove1, fullMove2, previousFullMove}, 935 ) 936 ) 937 self.data.status = sp.variant.finished("draw") 938 939 @sp.entrypoint 940 def offer_draw(self): 941 """Offer / acccept a draw agrement 942 A player may offer a draw at any stage. 943 If the opponent accepts, the game is a draw. 944 A draw offering cannot be retracted. 945 A draw offering is denied by calling the `deny_draw` entrypoint or by playing a move. 946 """ 947 assert not self.data.status.is_variant.finished(), "Game finished" 948 assert self.data.players.contains(sp.sender), "Wrong player" 949 self.data.draw_offer.add(sp.sender) 950 if sp.len(self.data.draw_offer) == 2: 951 self.data.status = sp.variant.finished("draw") 952 953 @sp.entrypoint 954 def claim_stalemate(self): 955 assert ( 956 sp.sender == self.data.players_map[self.data.board_state.nextPlayer] 957 ), "Wrong player" 958 assert self.data.status.is_variant.play() 959 self.data.status = sp.variant.claim_stalemate() 960 self.data.board_state.nextPlayer *= -1 961 962 @sp.entrypoint 963 def answer_stalemate(self, answer): 964 sp.cast(answer, sp.variant(accept=sp.unit, refuse=t_move)) 965 assert ( 966 sp.sender == self.data.players_map[self.data.board_state.nextPlayer] 967 ), "Wrong player" 968 assert self.data.status.is_variant.claim_stalemate() 969 with sp.match(answer): 970 with sp.case.accept: 971 self.data.status = sp.variant.finished("draw") 972 with sp.case.refuse as refuse_move: 973 # TODO: check if we need to update board_state with return value. 974 _ = self.move_piece( 975 sp.record( 976 board_state=self.data.board_state, 977 move=refuse_move, 978 get_movable_to=self.get_movable_to, 979 ) 980 ) 981 self.data.board_state.nextPlayer *= -1 982 self.data.status = sp.variant.force_play() 983 984 @sp.entrypoint 985 def play(self, move): 986 """ 987 move: Record(f: Record(i: Nat, j: Nat), t: Record(i: Nat, j: Nat)) 988 f: from square 989 t: destination square 990 991 claim_repeat: Option(TPair(nat, nat)) 992 Perform a threefold repetition claim after the move has been done 993 params: 2 other identical fullMove number 994 995 promotion: Option(Nat): 996 2: ROOK 997 3: KNIGHT 998 4: BISHOP 999 5: QUEEN 1000 """ 1001 assert ( 1002 sp.sender == self.data.players_map[self.data.board_state.nextPlayer] 1003 ), "Wrong player" 1004 assert ( 1005 self.data.status.is_variant.play() 1006 or self.data.status.is_variant.force_play() 1007 ) 1008 (board_state, is_draw) = self.move_piece( 1009 sp.record( 1010 board_state=self.data.board_state, 1011 move=move, 1012 get_movable_to=self.get_movable_to, 1013 ) 1014 ) 1015 new_board_state = board_state 1016 new_board_state.nextPlayer *= -1 1017 self.data.board_state = new_board_state 1018 if is_draw: 1019 self.data.status = sp.variant.finished("draw") 1020 self.data.draw_offer = set() 1021 1022 @sp.entrypoint 1023 def claim_checkmate(self): 1024 board_state = self.data.board_state 1025 if self.is_checkmate( 1026 sp.record(board_state=board_state, get_movable_to=self.get_movable_to) 1027 ): 1028 if board_state.nextPlayer == 1: 1029 self.data.status = sp.variant.finished("player_2_won") 1030 else: 1031 self.data.status = sp.variant.finished("player_1_won") 1032 else: 1033 raise "NotCheckmate" 1034 1035 1036def build_fen(c): 1037 return sp.View(c, "build_fen")() 1038 1039 1040def play(f, t, promotion=None, claim_repeat=None): 1041 return sp.record(f=f, t=t, promotion=promotion, claim_repeat=claim_repeat) 1042 1043 1044# Tests 1045if "main" in __name__: 1046 player1 = sp.test_account("player1") 1047 player2 = sp.test_account("player2") 1048 1049 @sp.add_test() 1050 def test(): 1051 sc = sp.test_scenario("Chess - Promotion", main) 1052 c1 = main.Chess(player1.address, player2.address) 1053 1054 sc.h1("Promotion") 1055 sc += c1 1056 1057 c1.play(play(f=sp.record(i=1, j=6), t=sp.record(i=3, j=6)), _sender=player1) 1058 c1.play(play(f=sp.record(i=6, j=1), t=sp.record(i=4, j=1)), _sender=player2) 1059 c1.play(play(f=sp.record(i=3, j=6), t=sp.record(i=4, j=6)), _sender=player1) 1060 c1.play(play(f=sp.record(i=4, j=1), t=sp.record(i=3, j=1)), _sender=player2) 1061 c1.play(play(f=sp.record(i=4, j=6), t=sp.record(i=5, j=6)), _sender=player1) 1062 c1.play(play(f=sp.record(i=3, j=1), t=sp.record(i=2, j=1)), _sender=player2) 1063 c1.play(play(f=sp.record(i=5, j=6), t=sp.record(i=6, j=7)), _sender=player1) 1064 c1.play(play(f=sp.record(i=2, j=1), t=sp.record(i=1, j=0)), _sender=player2) 1065 c1.play( 1066 play(f=sp.record(i=6, j=7), t=sp.record(i=7, j=6), promotion=sp.Some(5)), 1067 _sender=player1, 1068 ) 1069 c1.play( 1070 play(f=sp.record(i=1, j=0), t=sp.record(i=0, j=1), promotion=sp.Some(3)), 1071 _sender=player2, 1072 ) 1073 1074 sc.show(build_fen(c1)) 1075 sc.verify( 1076 build_fen(c1) == "rnbqkbQr/p1ppppp1/8/8/8/8/1PPPPP1P/RnBQKBNR w KQkq - 0 6" 1077 )
def
build_fen(c):
def
play(f, t, promotion=None, claim_repeat=None):