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):
1037def build_fen(c):
1038    return sp.View(c, "build_fen")()
def play(f, t, promotion=None, claim_repeat=None):
1041def play(f, t, promotion=None, claim_repeat=None):
1042    return sp.record(f=f, t=t, promotion=promotion, claim_repeat=claim_repeat)