diff --git a/chess/__init__.py b/chess/__init__.py index 7fe4cb9c..d22eae74 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -1146,6 +1146,68 @@ def board_fen(self, *, promoted: Optional[bool] = None) -> str: builder: List[str] = [] empty = 0 + if promoted is None: + promoted_mask = self._effective_promoted() + elif promoted: + promoted_mask = self.promoted + else: + promoted_mask = BB_EMPTY + + # Fast path for the standard implementation: read directly from the + # board bitboards and avoid allocating Piece objects per occupied square. + # If piece_at() is overridden in a subclass, fall back to the original + # behavior so custom piece lookup semantics are preserved. + if ( + type(self).piece_at is BaseBoard.piece_at and + type(self).piece_type_at is BaseBoard.piece_type_at + ): + append = builder.append + occupied = self.occupied + occupied_white = self.occupied_co[WHITE] + pawns = self.pawns + knights = self.knights + bishops = self.bishops + rooks = self.rooks + queens = self.queens + + for square in SQUARES_180: + mask = BB_SQUARES[square] + + if not occupied & mask: + empty += 1 + else: + if empty: + append(str(empty)) + empty = 0 + + color_is_white = bool(occupied_white & mask) + + if pawns & mask: + append("P" if color_is_white else "p") + elif knights & mask: + append("N" if color_is_white else "n") + elif bishops & mask: + append("B" if color_is_white else "b") + elif rooks & mask: + append("R" if color_is_white else "r") + elif queens & mask: + append("Q" if color_is_white else "q") + else: + append("K" if color_is_white else "k") + + if mask & promoted_mask: + append("~") + + if mask & BB_FILE_H: + if empty: + append(str(empty)) + empty = 0 + + if square != H1: + append("/") + + return "".join(builder) + for square in SQUARES_180: piece = self.piece_at(square) @@ -1157,12 +1219,6 @@ def board_fen(self, *, promoted: Optional[bool] = None) -> str: empty = 0 builder.append(piece.symbol()) - if promoted is None: - promoted_mask = self._effective_promoted() - elif promoted: - promoted_mask = self.promoted - else: - promoted_mask = BB_EMPTY if BB_SQUARES[square] & promoted_mask: builder.append("~") diff --git a/test.py b/test.py index 4927a2b8..de02f65e 100755 --- a/test.py +++ b/test.py @@ -1775,6 +1775,24 @@ def test_set_piece_map(self): a.set_piece_map({}) self.assertNotEqual(a, b) + def test_board_fen_respects_piece_at_override(self): + class OverrideBoard(chess.BaseBoard): + def piece_at(self, square): + if square == chess.E4: + return chess.Piece(chess.KING, chess.WHITE) + return None + + self.assertEqual(OverrideBoard.empty().board_fen(), "8/8/8/8/4K3/8/8/8") + + def test_board_fen_respects_piece_type_at_override(self): + class OverrideBoard(chess.BaseBoard): + def piece_type_at(self, square): + if square == chess.E4: + return chess.KING + return None + + self.assertEqual(OverrideBoard.empty().board_fen(), "8/8/8/8/4k3/8/8/8") + class SquareSetTestCase(unittest.TestCase):