1from pychess.Utils.const import EMPTY, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, \
2    ATOMICCHESS, BUGHOUSECHESS, CRAZYHOUSECHESS, CAMBODIANCHESS, MAKRUKCHESS, \
3    FISCHERRANDOMCHESS, SITTUYINCHESS, WILDCASTLECHESS, WILDCASTLESHUFFLECHESS, \
4    SUICIDECHESS, GIVEAWAYCHESS, DROP_VARIANTS, BLACK, WHITE, FAN_PIECES, NULL_MOVE, CAS_FLAGS, \
5    NORMALCHESS, PLACEMENTCHESS, THREECHECKCHESS, SETUPCHESS, FEN_START, \
6    chrU2Sign, cordDic, reprCord, reprFile, reprSign, reprSignMakruk, reprSignSittuyin, \
7    A1, A8, B1, B8, \
8    C1, C8, D1, D8, \
9    E1, E8, F1, F8, \
10    G1, G8, H1, H8, \
11    KING_CASTLE, QUEEN_CASTLE, DROP, PROMOTIONS, ENPASSANT, B_OO, B_OOO, W_OO, W_OOO
12from pychess.Utils.repr import reprColor
13from .ldata import FILE, fileBits
14from .attack import isAttacked
15from .bitboard import clearBit, setBit, bitPosArray
16from .PolyglotHash import pieceHashes, epHashes, \
17    W_OOHash, W_OOOHash, B_OOHash, B_OOOHash, colorHash, holdingHash
18
19################################################################################
20# FEN                                                                          #
21################################################################################
22
23# This will cause applyFen to raise an exception, if halfmove clock and fullmove
24# number is not specified
25STRICT_FEN = False
26
27################################################################################
28# LBoard                                                                       #
29################################################################################
30
31
32class LBoard:
33    __hash__ = None
34
35    ini_kings = (E1, E8)
36    ini_rooks = ((A1, H1), (A8, H8))
37
38    # Final positions of castled kings and rooks
39    fin_kings = ((C1, G1), (C8, G8))
40    fin_rooks = ((D1, F1), (D8, F8))
41
42    holding = ({PAWN: 0,
43                KNIGHT: 0,
44                BISHOP: 0,
45                ROOK: 0,
46                QUEEN: 0,
47                KING: 0},
48               {PAWN: 0,
49                KNIGHT: 0,
50                BISHOP: 0,
51                ROOK: 0,
52                QUEEN: 0,
53                KING: 0})
54
55    def __init__(self, variant=NORMALCHESS):
56        self.variant = variant
57
58        self.nags = []
59        # children can contain comments and variations
60        # variations are lists of lboard objects
61        self.children = []
62
63        # the next and prev lboard objects in the variation list
64        self.next = None
65        self.prev = None
66
67        # The high level owner Board (with Piece objects) in gamemodel
68        self.pieceBoard = None
69
70        # This will True except in so called null_board
71        # null_board act as parent of the variation
72        # when we add a variation to last played board from hint panel
73        self.fen_was_applied = False
74        self.plyCount = 0
75
76    @property
77    def lastMove(self):
78        return self.hist_move[-1] if self.fen_was_applied and len(
79            self.hist_move) > 0 else None
80
81    def repetitionCount(self, draw_threshold=3):
82        rc = 1
83        for ply in range(4, 1 + min(len(self.hist_hash), self.fifty), 2):
84            if self.hist_hash[-ply] == self.hash:
85                rc += 1
86                if rc >= draw_threshold:
87                    break
88        return rc
89
90    def iniAtomic(self):
91        self.hist_exploding_around = []
92
93    def iniHouse(self):
94        self.promoted = [0] * 64
95        self.capture_promoting = False
96        self.hist_capture_promoting = []
97        self.holding = ({PAWN: 0,
98                         KNIGHT: 0,
99                         BISHOP: 0,
100                         ROOK: 0,
101                         QUEEN: 0,
102                         KING: 0},
103                        {PAWN: 0,
104                         KNIGHT: 0,
105                         BISHOP: 0,
106                         ROOK: 0,
107                         QUEEN: 0,
108                         KING: 0})
109
110    def iniCambodian(self):
111        self.ini_kings = (D1, E8)
112        self.ini_queens = (E1, D8)
113        self.is_first_move = {KING: [True, True], QUEEN: [True, True]}
114        self.hist_is_first_move = []
115
116    def applyFen(self, fenstr):
117        """ Applies the fenstring to the board.
118            If the string is not properly
119            written a SyntaxError will be raised, having its message ending in
120            Pos(%d) specifying the string index of the problem.
121            if an error is found, no changes will be made to the board. """
122
123        assert not self.fen_was_applied, "The applyFen() method can be used on new LBoard objects only!"
124
125        # Set board to empty on Black's turn (which Polyglot-hashes to 0)
126        self.blocker = 0
127
128        self.friends = [0, 0]
129        self.kings = [-1, -1]
130
131        # this variable is a 2-dimmensionnal array, each case containing a bitboard
132        # self.boards[color] contains an array of bitboards, each representing the position of the pieces
133        # use example : self.boards[color][KNIGHT]
134        self.boards = ([0] * 7, [0] * 7)
135
136        self.enpassant = None  # cord which can be captured by enpassant or None
137        self.color = BLACK
138        self.castling = 0  # The castling availability in the position
139        self.hasCastled = [False, False]
140        self.fifty = 0  # A ply counter for the fifty moves rule
141        self.plyCount = 0
142
143        self.checked = None
144        self.opchecked = None
145
146        self.arBoard = [0] * 64
147
148        self.hash = 0
149        self.pawnhash = 0
150
151        #  Data from the position's history:
152        self.hist_move = []  # The move that was applied to get the position
153        self.hist_tpiece = []
154        # The piece the move captured, == EMPTY for normal moves
155        self.hist_enpassant = []
156        self.hist_castling = []
157        self.hist_hash = []
158        self.hist_fifty = []
159        self.hist_checked = []
160        self.hist_opchecked = []
161
162        # piece counts
163        self.pieceCount = ([0] * 7, [0] * 7)
164
165        # initial cords of rooks and kings for castling in Chess960
166        if self.variant == FISCHERRANDOMCHESS:
167            self.ini_kings = [None, None]
168            self.ini_rooks = ([None, None], [None, None])
169
170        elif self.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS):
171            self.ini_kings = [None, None]
172            self.fin_kings = ([None, None], [None, None])
173            self.fin_rooks = ([None, None], [None, None])
174
175        elif self.variant == ATOMICCHESS:
176            self.iniAtomic()
177
178        elif self.variant == THREECHECKCHESS:
179            self.remaining_checks = [3, 3]
180
181        elif self.variant == CAMBODIANCHESS:
182            self.iniCambodian()
183
184        if self.variant in DROP_VARIANTS:
185            self.iniHouse()
186
187            # Get information
188        parts = fenstr.split()
189        castChr = "-"
190        epChr = "-"
191        fiftyChr = "0"
192        moveNoChr = "1"
193        if STRICT_FEN and len(parts) != 6:
194            raise SyntaxError(_("FEN needs 6 data fields. \n\n%s") % fenstr)
195        elif len(parts) < 2:
196            raise SyntaxError(
197                _("FEN needs at least 2 data fields in fenstr. \n\n%s") %
198                fenstr)
199        elif len(parts) >= 7 and self.variant == THREECHECKCHESS:
200            pieceChrs, colChr, castChr, epChr, checksChr, fiftyChr, moveNoChr = parts[:7]
201            self.remaining_checks = list(map(int, checksChr.split("+")))
202        elif len(parts) >= 6:
203            pieceChrs, colChr, castChr, epChr, fiftyChr, moveNoChr = parts[:6]
204        elif len(parts) == 5:
205            pieceChrs, colChr, castChr, epChr, fiftyChr = parts
206        elif len(parts) == 4:
207            if parts[2].isdigit() and parts[3].isdigit():
208                # xboard FEN usage for asian variants
209                pieceChrs, colChr, fiftyChr, moveNoChr = parts
210            else:
211                pieceChrs, colChr, castChr, epChr = parts
212        elif len(parts) == 3:
213            pieceChrs, colChr, castChr = parts
214        else:
215            pieceChrs, colChr = parts
216
217        # Try to validate some information
218        # TODO: This should be expanded and perhaps moved
219
220        slashes = pieceChrs.count("/")
221        if slashes < 7:
222            raise SyntaxError(
223                _("Needs 7 slashes in piece placement field. \n\n%s") % fenstr)
224
225        if not colChr.lower() in ("w", "b"):
226            raise SyntaxError(
227                _("Active color field must be one of w or b. \n\n%s") % fenstr)
228
229        if castChr != "-":
230            for Chr in castChr:
231                valid_chars = "ABCDEFGHKQ" if self.variant == FISCHERRANDOMCHESS or self.variant == SETUPCHESS else "KQ"
232                if Chr.upper() not in valid_chars:
233                    if self.variant == CAMBODIANCHESS:
234                        pass
235                        # sjaakii uses DEde in cambodian starting fen to indicate
236                        # that queens and kings are virgins (not moved yet)
237                    else:
238                        raise SyntaxError(_("Castling availability field is not legal. \n\n%s")
239                                          % fenstr)
240
241        if epChr != "-" and epChr not in cordDic:
242            raise SyntaxError(_("En passant cord is not legal. \n\n%s") %
243                              fenstr)
244
245        # Parse piece placement field
246        promoted = False
247        # if there is a holding within [] we change it to BFEN style first
248        if pieceChrs.endswith("]"):
249            pieceChrs = pieceChrs[:-1].replace("[", "/").replace("-", "")
250        for r, rank in enumerate(pieceChrs.split("/")):
251            cord = (7 - r) * 8
252            for char in rank:
253                if r > 7:
254                    # After the 8.rank BFEN can contain holdings (captured pieces)
255                    # "~" after a piece letter denotes promoted piece
256                    if r == 8 and self.variant in DROP_VARIANTS:
257                        color = char.islower() and BLACK or WHITE
258                        piece = chrU2Sign[char.upper()]
259                        self.holding[color][piece] += 1
260                        self.hash ^= holdingHash[color][piece][self.holding[color][piece]]
261                        continue
262                    else:
263                        break
264
265                if char.isdigit():
266                    cord += int(char)
267                elif char == "~":
268                    promoted = True
269                else:
270                    color = char.islower() and BLACK or WHITE
271                    piece = chrU2Sign[char.upper()]
272                    self._addPiece(cord, piece, color)
273                    self.pieceCount[color][piece] += 1
274
275                    if self.variant in DROP_VARIANTS and promoted:
276                        self.promoted[cord] = 1
277                        promoted = False
278
279                    if self.variant == CAMBODIANCHESS:
280                        if piece == KING and self.kings[
281                                color] != self.ini_kings[color]:
282                            self.is_first_move[KING][color] = False
283                        if piece == QUEEN and cord != self.ini_queens[color]:
284                            self.is_first_move[QUEEN][color] = False
285
286                    cord += 1
287
288            if self.variant == FISCHERRANDOMCHESS:
289                # Save ranks fo find outermost rooks
290                # if KkQq was used in castling rights
291                if r == 0:
292                    rank8 = rank
293                elif r == 7:
294                    rank1 = rank
295
296        # Parse active color field
297
298        if colChr.lower() == "w":
299            self.setColor(WHITE)
300        else:
301            self.setColor(BLACK)
302
303        # Parse castling availability
304
305        castling = 0
306        for char in castChr:
307            if self.variant == FISCHERRANDOMCHESS:
308                if char in reprFile:
309                    if char < reprCord[self.kings[BLACK]][0]:
310                        castling |= B_OOO
311                        self.ini_rooks[1][0] = reprFile.index(char) + 56
312                    else:
313                        castling |= B_OO
314                        self.ini_rooks[1][1] = reprFile.index(char) + 56
315                elif char in [c.upper() for c in reprFile]:
316                    if char < reprCord[self.kings[WHITE]][0].upper():
317                        castling |= W_OOO
318                        self.ini_rooks[0][0] = reprFile.index(char.lower())
319                    else:
320                        castling |= W_OO
321                        self.ini_rooks[0][1] = reprFile.index(char.lower())
322                elif char == "K":
323                    castling |= W_OO
324                    self.ini_rooks[0][1] = rank1.rfind('R')
325                elif char == "Q":
326                    castling |= W_OOO
327                    self.ini_rooks[0][0] = rank1.find('R')
328                elif char == "k":
329                    castling |= B_OO
330                    self.ini_rooks[1][1] = rank8.rfind('r') + 56
331                elif char == "q":
332                    castling |= B_OOO
333                    self.ini_rooks[1][0] = rank8.find('r') + 56
334            else:
335                if char == "K":
336                    castling |= W_OO
337                elif char == "Q":
338                    castling |= W_OOO
339                elif char == "k":
340                    castling |= B_OO
341                elif char == "q":
342                    castling |= B_OOO
343
344        if self.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS,
345                            FISCHERRANDOMCHESS):
346            self.ini_kings[WHITE] = self.kings[WHITE]
347            self.ini_kings[BLACK] = self.kings[BLACK]
348            if self.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS):
349                if self.ini_kings[WHITE] == D1 and self.ini_kings[BLACK] == D8:
350                    self.fin_kings = ([B1, F1], [B8, F8])
351                    self.fin_rooks = ([C1, E1], [C8, E8])
352                elif self.ini_kings[WHITE] == D1:
353                    self.fin_kings = ([B1, F1], [C8, G8])
354                    self.fin_rooks = ([C1, E1], [D8, F8])
355                elif self.ini_kings[BLACK] == D8:
356                    self.fin_kings = ([C1, G1], [B8, F8])
357                    self.fin_rooks = ([D1, F1], [C8, E8])
358                else:
359                    self.fin_kings = ([C1, G1], [C8, G8])
360                    self.fin_rooks = ([D1, F1], [D8, F8])
361
362        self.setCastling(castling)
363
364        # Parse en passant target sqaure
365
366        if epChr == "-":
367            self.setEnpassant(None)
368        else:
369            self.setEnpassant(cordDic[epChr])
370
371        # Parse halfmove clock field
372
373        if fiftyChr.isdigit():
374            self.fifty = int(fiftyChr)
375        else:
376            self.fifty = 0
377
378        # Parse fullmove number
379
380        if moveNoChr.isdigit():
381            movenumber = max(int(moveNoChr), 1) * 2 - 2
382            if self.color == BLACK:
383                movenumber += 1
384            self.plyCount = movenumber
385        else:
386            self.plyCount = 1
387
388        self.fen_was_applied = True
389
390    def isChecked(self):
391        if self.variant == SUICIDECHESS or self.variant == GIVEAWAYCHESS:
392            return False
393        elif self.variant == ATOMICCHESS:
394            if not self.boards[self.color][KING]:
395                return False
396            if -2 < (self.kings[0] >> 3) - (self.kings[1] >> 3) < 2 and -2 < (self.kings[0] & 7) - (self.kings[1] & 7) < 2:
397                return False
398        if self.checked is None:
399            kingcord = self.kings[self.color]
400            if kingcord == -1:
401                return False
402            self.checked = isAttacked(self,
403                                      kingcord,
404                                      1 - self.color,
405                                      ischecked=True)
406        return self.checked
407
408    def opIsChecked(self):
409        if self.variant == SUICIDECHESS or self.variant == GIVEAWAYCHESS:
410            return False
411        elif self.variant == ATOMICCHESS:
412            if not self.boards[1 - self.color][KING]:
413                return False
414            if -2 < (self.kings[0] >> 3) - (self.kings[1] >> 3) < 2 and -2 < (self.kings[0] & 7) - (self.kings[1] & 7) < 2:
415                return False
416        if self.opchecked is None:
417            kingcord = self.kings[1 - self.color]
418            if kingcord == -1:
419                return False
420            self.opchecked = isAttacked(self,
421                                        kingcord,
422                                        self.color,
423                                        ischecked=True)
424        return self.opchecked
425
426    def willLeaveInCheck(self, move):
427        if self.variant == SUICIDECHESS or self.variant == GIVEAWAYCHESS:
428            return False
429        board_clone = self.clone()
430        board_clone.applyMove(move)
431        return board_clone.opIsChecked()
432
433    def willGiveCheck(self, move):
434        board_clone = self.clone()
435        board_clone.applyMove(move)
436        return board_clone.isChecked()
437
438    def _addPiece(self, cord, piece, color):
439        _setBit = setBit
440        self.boards[color][piece] = _setBit(self.boards[color][piece], cord)
441        self.friends[color] = _setBit(self.friends[color], cord)
442        self.blocker = _setBit(self.blocker, cord)
443
444        if piece == PAWN:
445            self.pawnhash ^= pieceHashes[color][PAWN][cord]
446        elif piece == KING:
447            self.kings[color] = cord
448        self.hash ^= pieceHashes[color][piece][cord]
449        self.arBoard[cord] = piece
450
451    def _removePiece(self, cord, piece, color):
452        _clearBit = clearBit
453        self.boards[color][piece] = _clearBit(self.boards[color][piece], cord)
454        self.friends[color] = _clearBit(self.friends[color], cord)
455        self.blocker = _clearBit(self.blocker, cord)
456
457        if piece == PAWN:
458            self.pawnhash ^= pieceHashes[color][PAWN][cord]
459
460        self.hash ^= pieceHashes[color][piece][cord]
461        self.arBoard[cord] = EMPTY
462
463    def setColor(self, color):
464        if color == self.color:
465            return
466        self.color = color
467        self.hash ^= colorHash
468
469    def setCastling(self, castling):
470        if self.castling == castling:
471            return
472
473        if castling & W_OO != self.castling & W_OO:
474            self.hash ^= W_OOHash
475        if castling & W_OOO != self.castling & W_OOO:
476            self.hash ^= W_OOOHash
477        if castling & B_OO != self.castling & B_OO:
478            self.hash ^= B_OOHash
479        if castling & B_OOO != self.castling & B_OOO:
480            self.hash ^= B_OOOHash
481
482        self.castling = castling
483
484    def setEnpassant(self, epcord):
485        # Strip the square if there's no adjacent enemy pawn to make the capture
486        if epcord is not None:
487            sideToMove = (epcord >> 3 == 2 and BLACK or WHITE)
488            fwdPawns = self.boards[sideToMove][PAWN]
489            if sideToMove == WHITE:
490                fwdPawns >>= 8
491            else:
492                fwdPawns <<= 8
493            pawnTargets = (fwdPawns & ~fileBits[0]) << 1
494            pawnTargets |= (fwdPawns & ~fileBits[7]) >> 1
495            if not pawnTargets & bitPosArray[epcord]:
496                epcord = None
497
498        if self.enpassant == epcord:
499            return
500        if self.enpassant is not None:
501            self.hash ^= epHashes[self.enpassant & 7]
502        if epcord is not None:
503            self.hash ^= epHashes[epcord & 7]
504        self.enpassant = epcord
505
506    # @profile
507    def applyMove(self, move):
508        flag = move >> 12
509
510        fcord = (move >> 6) & 63
511        tcord = move & 63
512
513        fpiece = fcord if flag == DROP else self.arBoard[fcord]
514        tpiece = self.arBoard[tcord]
515
516        color = self.color
517        opcolor = 1 - self.color
518        castling = self.castling
519
520        self.hist_move.append(move)
521        self.hist_enpassant.append(self.enpassant)
522        self.hist_castling.append(self.castling)
523        self.hist_hash.append(self.hash)
524        self.hist_fifty.append(self.fifty)
525        self.hist_checked.append(self.checked)
526        self.hist_opchecked.append(self.opchecked)
527        if self.variant in DROP_VARIANTS:
528            self.hist_capture_promoting.append(self.capture_promoting)
529        if self.variant == CAMBODIANCHESS:
530            self.hist_is_first_move.append({KING: self.is_first_move[KING][:],
531                                            QUEEN: self.is_first_move[QUEEN][:]})
532
533        self.opchecked = None
534        self.checked = None
535
536        if flag == NULL_MOVE:
537            self.setColor(opcolor)
538            self.plyCount += 1
539            return move
540
541        if self.variant == CAMBODIANCHESS:
542            if fpiece == KING and self.is_first_move[KING][color]:
543                self.is_first_move[KING][color] = False
544            elif fpiece == QUEEN and self.is_first_move[QUEEN][color]:
545                self.is_first_move[QUEEN][color] = False
546
547        # Castling moves can be represented strangely, so normalize them.
548        if flag in (KING_CASTLE, QUEEN_CASTLE):
549            side = flag - QUEEN_CASTLE
550            fpiece = KING
551            tpiece = EMPTY  # In FRC, there may be a rook there, but the king doesn't capture it.
552            fcord = self.ini_kings[color]
553            if FILE(fcord) == 3 and self.variant in (WILDCASTLECHESS,
554                                                     WILDCASTLESHUFFLECHESS):
555                side = 0 if side == 1 else 1
556            tcord = self.fin_kings[color][side]
557            rookf = self.ini_rooks[color][side]
558            rookt = self.fin_rooks[color][side]
559
560        # Capture (sittuyin in place promotion is not capture move!)
561        if tpiece != EMPTY and fcord != tcord:
562            self._removePiece(tcord, tpiece, opcolor)
563            self.pieceCount[opcolor][tpiece] -= 1
564            if self.variant in DROP_VARIANTS:
565                if self.promoted[tcord]:
566                    if self.variant == CRAZYHOUSECHESS:
567                        self.holding[color][PAWN] += 1
568                        self.hash ^= holdingHash[color][PAWN][self.holding[color][PAWN]]
569                    self.capture_promoting = True
570                else:
571                    if self.variant == CRAZYHOUSECHESS:
572                        self.holding[color][tpiece] += 1
573                        self.hash ^= holdingHash[color][tpiece][self.holding[color][tpiece]]
574                    self.capture_promoting = False
575            elif self.variant == ATOMICCHESS:
576                from pychess.Variants.atomic import piecesAround
577                apieces = [(fcord, fpiece, color), ]
578                for acord, apiece, acolor in piecesAround(self, tcord):
579                    if apiece != PAWN and acord != fcord:
580                        self._removePiece(acord, apiece, acolor)
581                        self.pieceCount[acolor][apiece] -= 1
582                        apieces.append((acord, apiece, acolor))
583                    if apiece == ROOK and acord != fcord:
584                        if acord == self.ini_rooks[opcolor][0]:
585                            castling &= ~CAS_FLAGS[opcolor][0]
586                        elif acord == self.ini_rooks[opcolor][1]:
587                            castling &= ~CAS_FLAGS[opcolor][1]
588                self.hist_exploding_around.append(apieces)
589        self.hist_tpiece.append(tpiece)
590
591        # Remove moving piece(s), then add them at their destination.
592        if flag == DROP:
593            if self.variant in DROP_VARIANTS:
594                assert self.holding[color][fpiece] > 0
595            self.holding[color][fpiece] -= 1
596            self.hash ^= holdingHash[color][fpiece][self.holding[color][fpiece]]
597            self.pieceCount[color][fpiece] += 1
598        else:
599            self._removePiece(fcord, fpiece, color)
600
601        if flag in (KING_CASTLE, QUEEN_CASTLE):
602            self._removePiece(rookf, ROOK, color)
603            self._addPiece(rookt, ROOK, color)
604            self.hasCastled[color] = True
605
606        if flag == ENPASSANT:
607            takenPawnC = tcord + (color == WHITE and -8 or 8)
608            self._removePiece(takenPawnC, PAWN, opcolor)
609            self.pieceCount[opcolor][PAWN] -= 1
610            if self.variant == CRAZYHOUSECHESS:
611                self.holding[color][PAWN] += 1
612                self.hash ^= holdingHash[color][PAWN][self.holding[color][PAWN]]
613            elif self.variant == ATOMICCHESS:
614                from pychess.Variants.atomic import piecesAround
615                apieces = [(fcord, fpiece, color), ]
616                for acord, apiece, acolor in piecesAround(self, tcord):
617                    if apiece != PAWN and acord != fcord:
618                        self._removePiece(acord, apiece, acolor)
619                        self.pieceCount[acolor][apiece] -= 1
620                        apieces.append((acord, apiece, acolor))
621                self.hist_exploding_around.append(apieces)
622        elif flag in PROMOTIONS:
623            # Pretend the pawn changes into a piece before reaching its destination.
624            fpiece = flag - 2
625            self.pieceCount[color][fpiece] += 1
626            self.pieceCount[color][PAWN] -= 1
627
628        if self.variant in DROP_VARIANTS:
629            if tpiece == EMPTY:
630                self.capture_promoting = False
631
632            if flag in PROMOTIONS:
633                self.promoted[tcord] = 1
634            elif flag != DROP:
635                if self.promoted[fcord]:
636                    self.promoted[fcord] = 0
637                    self.promoted[tcord] = 1
638                elif tpiece != EMPTY:
639                    self.promoted[tcord] = 0
640
641        if self.variant == ATOMICCHESS and (tpiece != EMPTY or
642                                            flag == ENPASSANT):
643            self.pieceCount[color][fpiece] -= 1
644        else:
645            self._addPiece(tcord, fpiece, color)
646
647        if fpiece == PAWN and abs(fcord - tcord) == 16:
648            self.setEnpassant((fcord + tcord) // 2)
649        else:
650            self.setEnpassant(None)
651
652        if tpiece == EMPTY and fpiece != PAWN:
653            self.fifty += 1
654        else:
655            self.fifty = 0
656
657        # Clear castle flags
658        king = self.ini_kings[color]
659        wildcastle = FILE(king) == 3 and self.variant in (
660            WILDCASTLECHESS, WILDCASTLESHUFFLECHESS)
661        if fpiece == KING:
662            castling &= ~CAS_FLAGS[color][0]
663            castling &= ~CAS_FLAGS[color][1]
664        elif fpiece == ROOK:
665            if fcord == self.ini_rooks[color][0]:
666                side = 1 if wildcastle else 0
667                castling &= ~CAS_FLAGS[color][side]
668            elif fcord == self.ini_rooks[color][1]:
669                side = 0 if wildcastle else 1
670                castling &= ~CAS_FLAGS[color][side]
671        if tpiece == ROOK:
672            if tcord == self.ini_rooks[opcolor][0]:
673                side = 1 if wildcastle else 0
674                castling &= ~CAS_FLAGS[opcolor][side]
675            elif tcord == self.ini_rooks[opcolor][1]:
676                side = 0 if wildcastle else 1
677                castling &= ~CAS_FLAGS[opcolor][side]
678
679        if self.variant == PLACEMENTCHESS and self.plyCount == 15:
680            castling = 0
681            if self.arBoard[A1] == ROOK and self.arBoard[E1] == KING:
682                castling |= W_OOO
683            if self.arBoard[H1] == ROOK and self.arBoard[E1] == KING:
684                castling |= W_OO
685            if self.arBoard[A8] == ROOK and self.arBoard[E8] == KING:
686                castling |= B_OOO
687            if self.arBoard[H8] == ROOK and self.arBoard[E8] == KING:
688                castling |= B_OO
689
690        self.setCastling(castling)
691
692        self.setColor(opcolor)
693        self.plyCount += 1
694
695    def popMove(self):
696        # Note that we remove the last made move, which was not made by boards
697        # current color, but by its opponent
698        color = 1 - self.color
699        opcolor = self.color
700
701        move = self.hist_move.pop()
702        cpiece = self.hist_tpiece.pop()
703
704        flag = move >> 12
705
706        if flag == NULL_MOVE:
707            self.setColor(color)
708            return
709
710        fcord = (move >> 6) & 63
711        tcord = move & 63
712        tpiece = self.arBoard[tcord]
713
714        # Castling moves can be represented strangely, so normalize them.
715        if flag in (KING_CASTLE, QUEEN_CASTLE):
716            side = flag - QUEEN_CASTLE
717            tpiece = KING
718            fcord = self.ini_kings[color]
719            if FILE(fcord) == 3 and self.variant in (WILDCASTLECHESS,
720                                                     WILDCASTLESHUFFLECHESS):
721                side = 0 if side == 1 else 1
722            tcord = self.fin_kings[color][side]
723            rookf = self.ini_rooks[color][side]
724            rookt = self.fin_rooks[color][side]
725            self._removePiece(tcord, tpiece, color)
726            self._removePiece(rookt, ROOK, color)
727            self._addPiece(rookf, ROOK, color)
728            self.hasCastled[color] = False
729        else:
730            self._removePiece(tcord, tpiece, color)
731
732        # Put back captured piece
733        if cpiece != EMPTY and fcord != tcord:
734            self._addPiece(tcord, cpiece, opcolor)
735            self.pieceCount[opcolor][cpiece] += 1
736            if self.variant == CRAZYHOUSECHESS:
737                if self.capture_promoting:
738                    assert self.holding[color][PAWN] > 0
739                    self.holding[color][PAWN] -= 1
740                    self.hash ^= holdingHash[color][PAWN][self.holding[color][PAWN]]
741                else:
742                    assert self.holding[color][cpiece] > 0
743                    self.holding[color][cpiece] -= 1
744                    self.hash ^= holdingHash[color][cpiece][self.holding[color][cpiece]]
745            elif self.variant == ATOMICCHESS:
746                apieces = self.hist_exploding_around.pop()
747                for acord, apiece, acolor in apieces:
748                    self._addPiece(acord, apiece, acolor)
749                    self.pieceCount[acolor][apiece] += 1
750
751                    # Put back piece captured by enpassant
752        if flag == ENPASSANT:
753            epcord = color == WHITE and tcord - 8 or tcord + 8
754            self._addPiece(epcord, PAWN, opcolor)
755            self.pieceCount[opcolor][PAWN] += 1
756            if self.variant == CRAZYHOUSECHESS:
757                assert self.holding[color][PAWN] > 0
758                self.holding[color][PAWN] -= 1
759                self.hash ^= holdingHash[color][PAWN][self.holding[color][PAWN]]
760            elif self.variant == ATOMICCHESS:
761                apieces = self.hist_exploding_around.pop()
762                for acord, apiece, acolor in apieces:
763                    self._addPiece(acord, apiece, acolor)
764                    self.pieceCount[acolor][apiece] += 1
765
766            # Un-promote pawn
767        if flag in PROMOTIONS:
768            tpiece = PAWN
769            self.pieceCount[color][flag - 2] -= 1
770            self.pieceCount[color][PAWN] += 1
771
772        # Put back moved piece
773        if flag == DROP:
774            self.holding[color][tpiece] += 1
775            self.hash ^= holdingHash[color][tpiece][self.holding[color][tpiece]]
776            self.pieceCount[color][tpiece] -= 1
777        else:
778            if not (self.variant == ATOMICCHESS and
779                    (cpiece != EMPTY or flag == ENPASSANT)):
780                self._addPiece(fcord, tpiece, color)
781
782        if self.variant in DROP_VARIANTS:
783            if flag != DROP:
784                if self.promoted[tcord] and (flag not in PROMOTIONS):
785                    self.promoted[fcord] = 1
786                if self.capture_promoting:
787                    self.promoted[tcord] = 1
788                else:
789                    self.promoted[tcord] = 0
790            self.capture_promoting = self.hist_capture_promoting.pop()
791
792        if self.variant == CAMBODIANCHESS:
793            self.is_first_move = self.hist_is_first_move.pop()
794
795        self.setColor(color)
796
797        self.checked = self.hist_checked.pop()
798        self.opchecked = self.hist_opchecked.pop()
799        self.enpassant = self.hist_enpassant.pop()
800        self.castling = self.hist_castling.pop()
801        self.hash = self.hist_hash.pop()
802        self.fifty = self.hist_fifty.pop()
803        self.plyCount -= 1
804
805    def __eq__(self, other):
806        if not (other is not None and
807                self.fen_was_applied and other.fen_was_applied and
808                self.hash == other.hash and self.plyCount == other.plyCount):
809            return False
810
811        b0, b1 = self.prev, other.prev
812        ok = True
813        while ok and b0 is not None and b1 is not None:
814            if not (b0.fen_was_applied and b1.fen_was_applied and
815                    b0.hash == b1.hash and b0.plyCount == b1.plyCount):
816                ok = False
817            else:
818                b0, b1 = b0.prev, b1.prev
819        return ok
820
821    def __ne__(self, other):
822        return not self.__eq__(other)
823
824    def reprCastling(self):
825        if not self.castling:
826            return "-"
827        else:
828            strs = []
829            if self.variant == FISCHERRANDOMCHESS:
830                if self.castling & W_OO:
831                    strs.append(reprCord[self.ini_rooks[0][1]][0].upper())
832                if self.castling & W_OOO:
833                    strs.append(reprCord[self.ini_rooks[0][0]][0].upper())
834                if self.castling & B_OO:
835                    strs.append(reprCord[self.ini_rooks[1][1]][0])
836                if self.castling & B_OOO:
837                    strs.append(reprCord[self.ini_rooks[1][0]][0])
838            else:
839                if self.castling & W_OO:
840                    strs.append("K")
841                if self.castling & W_OOO:
842                    strs.append("Q")
843                if self.castling & B_OO:
844                    strs.append("k")
845                if self.castling & B_OOO:
846                    strs.append("q")
847            return "".join(strs)
848
849    def prepr(self, ascii=False):
850        if not self.fen_was_applied:
851            return ("LBoard without applied FEN")
852        b = "#" + reprColor[self.color] + " "
853        b += self.reprCastling() + " "
854        b += self.enpassant is not None and reprCord[self.enpassant] or "-"
855        b += "\n# "
856        rows = [self.arBoard[i:i + 8] for i in range(0, 64, 8)][::-1]
857        for r, row in enumerate(rows):
858            for i, piece in enumerate(row):
859                if piece != EMPTY:
860                    if bitPosArray[(7 - r) * 8 + i] & self.friends[WHITE]:
861                        assert self.boards[WHITE][
862                            piece], "self.boards doesn't match self.arBoard !!!"
863                        sign = reprSign[piece] if ascii else FAN_PIECES[WHITE][
864                            piece]
865                    else:
866                        assert self.boards[BLACK][
867                            piece], "self.boards doesn't match self.arBoard !!!"
868                        sign = reprSign[piece].lower(
869                        ) if ascii else FAN_PIECES[BLACK][piece]
870                    b += sign
871                else:
872                    b += "."
873                b += " "
874            b += "\n# "
875
876        if self.variant in DROP_VARIANTS:
877            for color in (BLACK, WHITE):
878                holding = self.holding[color]
879                b += "\n# [%s]" % "".join([reprSign[
880                    piece] if ascii else FAN_PIECES[color][piece] * holding[
881                        piece] for piece in holding if holding[piece] > 0])
882        return b
883
884    def __repr__(self):
885        return self.prepr()
886
887    def asFen(self, enable_bfen=True):
888        fenstr = []
889
890        rows = [self.arBoard[i:i + 8] for i in range(0, 64, 8)][::-1]
891        for r, row in enumerate(rows):
892            empty = 0
893            for i, piece in enumerate(row):
894                if piece != EMPTY:
895                    if empty > 0:
896                        fenstr.append(str(empty))
897                        empty = 0
898                    if self.variant in (CAMBODIANCHESS, MAKRUKCHESS):
899                        sign = reprSignMakruk[piece]
900                    elif self.variant == SITTUYINCHESS:
901                        sign = reprSignSittuyin[piece]
902                    else:
903                        sign = reprSign[piece]
904                    if bitPosArray[(7 - r) * 8 + i] & self.friends[WHITE]:
905                        sign = sign.upper()
906                    else:
907                        sign = sign.lower()
908                    fenstr.append(sign)
909                    if self.variant in (BUGHOUSECHESS, CRAZYHOUSECHESS):
910                        if self.promoted[r * 8 + i]:
911                            fenstr.append("~")
912                else:
913                    empty += 1
914            if empty > 0:
915                fenstr.append(str(empty))
916            if r != 7:
917                fenstr.append("/")
918
919        if self.variant in DROP_VARIANTS:
920            holding_pieces = []
921            for color in (BLACK, WHITE):
922                holding = self.holding[color]
923                for piece in holding:
924                    if holding[piece] > 0:
925                        if self.variant == SITTUYINCHESS:
926                            sign = reprSignSittuyin[piece]
927                        else:
928                            sign = reprSign[piece]
929                        sign = sign.upper() if color == WHITE else sign.lower()
930                        holding_pieces.append(sign * holding[piece])
931            if holding_pieces:
932                if enable_bfen:
933                    fenstr.append("/")
934                    fenstr += holding_pieces
935                else:
936                    fenstr.append("[")
937                    fenstr += holding_pieces
938                    fenstr.append("]")
939
940        fenstr.append(" ")
941
942        fenstr.append(self.color == WHITE and "w" or "b")
943        fenstr.append(" ")
944
945        if self.variant == CAMBODIANCHESS:
946            cast = ""
947            if self.is_first_move[KING][WHITE]:
948                cast += "D"
949            if self.is_first_move[QUEEN][WHITE]:
950                cast += "E"
951            if self.is_first_move[KING][BLACK]:
952                cast += "d"
953            if self.is_first_move[QUEEN][BLACK]:
954                cast += "e"
955            if not cast:
956                cast = "-"
957            fenstr.append(cast)
958        else:
959            fenstr.append(self.reprCastling())
960        fenstr.append(" ")
961
962        if not self.enpassant:
963            fenstr.append("-")
964        else:
965            fenstr.append(reprCord[self.enpassant])
966        fenstr.append(" ")
967
968        fenstr.append(str(self.fifty))
969        fenstr.append(" ")
970
971        fullmove = (self.plyCount) // 2 + 1
972        fenstr.append(str(fullmove))
973
974        return "".join(fenstr)
975
976    def clone(self):
977        copy = LBoard(self.variant)
978        copy.blocker = self.blocker
979
980        copy.friends = self.friends[:]
981        copy.kings = self.kings[:]
982        copy.boards = (self.boards[WHITE][:], self.boards[BLACK][:])
983        copy.arBoard = self.arBoard[:]
984        copy.pieceCount = (self.pieceCount[WHITE][:], self.pieceCount[BLACK][:])
985
986        copy.color = self.color
987        copy.plyCount = self.plyCount
988        copy.hasCastled = self.hasCastled[:]
989
990        copy.enpassant = self.enpassant
991        copy.castling = self.castling
992        copy.hash = self.hash
993        copy.pawnhash = self.pawnhash
994        copy.fifty = self.fifty
995        copy.checked = self.checked
996        copy.opchecked = self.opchecked
997
998        copy.hist_move = self.hist_move[:]
999        copy.hist_tpiece = self.hist_tpiece[:]
1000        copy.hist_enpassant = self.hist_enpassant[:]
1001        copy.hist_castling = self.hist_castling[:]
1002        copy.hist_hash = self.hist_hash[:]
1003        copy.hist_fifty = self.hist_fifty[:]
1004        copy.hist_checked = self.hist_checked[:]
1005        copy.hist_opchecked = self.hist_opchecked[:]
1006
1007        if self.variant == FISCHERRANDOMCHESS:
1008            copy.ini_kings = self.ini_kings[:]
1009            copy.ini_rooks = (self.ini_rooks[0][:], self.ini_rooks[1][:])
1010        elif self.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS):
1011            copy.ini_kings = self.ini_kings[:]
1012            copy.fin_kings = (self.fin_kings[0][:], self.fin_kings[1][:])
1013            copy.fin_rooks = (self.fin_rooks[0][:], self.fin_rooks[1][:])
1014        elif self.variant in DROP_VARIANTS:
1015            copy.promoted = self.promoted[:]
1016            copy.holding = (self.holding[0].copy(), self.holding[1].copy())
1017            copy.capture_promoting = self.capture_promoting
1018            copy.hist_capture_promoting = self.hist_capture_promoting[:]
1019        elif self.variant == ATOMICCHESS:
1020            copy.hist_exploding_around = [a[:] for a in self.hist_exploding_around]
1021        elif self.variant == THREECHECKCHESS:
1022            copy.remaining_checks = self.remaining_checks[:]
1023        elif self.variant == CAMBODIANCHESS:
1024            copy.ini_kings = self.ini_kings
1025            copy.ini_queens = self.ini_queens
1026            copy.is_first_move = {KING: self.is_first_move[KING][:],
1027                                  QUEEN: self.is_first_move[QUEEN][:]}
1028            copy.hist_is_first_move = self.hist_is_first_move[:]
1029
1030        copy.fen_was_applied = self.fen_was_applied
1031        return copy
1032
1033
1034START_BOARD = LBoard()
1035START_BOARD.applyFen(FEN_START)
1036