0

CONTEXT

I'm making this chess engine in python and one aspect of the program is that the board is represented by 64-bit numbers called bitboards, so I created a class to do just that, here is a snippet:

from __future__ import annotations

FULL_MASK = 0xFFFFFFFFFFFFFFFF

class BitBoard:

    def __init__(self, board: int) -> None:
        self.__board: int = board
    
    @property
    def board(self) -> int:
        return self.__board & FULL_MASK
    
    @board.setter
    def board(self, value: int) -> None:
        self.__board = value & FULL_MASK
    
    def set_bit(self, square: int) -> None:
        self.__board |= (1 << square) & FULL_MASK
    
    def get_bit(self, square: int) -> None:
        self.__board &= (1 << square) & FULL_MASK
    
    def pop_bit(self, square: int) -> None:
        self.__board &= ~(1 << square) & FULL_MASK
    
    def __and__(self, value: int) -> BitBoard:
        return BitBoard((self.__board & value) & FULL_MASK)
    
    def __or__(self, value: int) -> BitBoard:
        return BitBoard((self.__board | value) & FULL_MASK)
    
    def __xor__(self, value: int) -> BitBoard:
        return BitBoard((self.__board ^ value) & FULL_MASK)
    
    def __inv__(self) -> BitBoard:
        return BitBoard(~self.__board & FULL_MASK)
    
    def __lshift__(self, value: int) -> BitBoard:
        return BitBoard((self.__board << value) & FULL_MASK)
    
    def __rshift__(self, value: int) -> BitBoard:
        return BitBoard((self.__board >> value) & FULL_MASK)

As shown I've overidden all bitwise operations. While the NOT, SHIFT-LEFT, and SHIFT-RIGHT are rarely and/or never used with other bitboards, the AND, OR and XOR operators sometimes are.

QUESTION

Using the AND operator as in example, I want it so that if a bitboard is ANDed with a literal, it will bitwise AND its board with that literal. similarly, if it is ANDed with another bitboard, it will bitwise AND its board with the other's board. is there a way to do this? preferably without external modules.

ATTEMPTS

I've tried

def __and__(self, value: int | BitBoard) -> BitBoard:
    result: BitBoard
        
    if type(value) == int:
        result = BitBoard((self.__board & value) & FULL_MASK)
    if type(value) == Bitboard:
        result = BitBoard((self.__board & value.board) & FULL_MASK)
        
    return result

and other similar things but everytime the type checker yells at me for doing them. I know there is a way to do it with metaclasses but that would be counter productive since the purpose of using bitboards is to reduce computation. I am aware of the multipledispatch module but I'm aiming for this project to be pure base python.

10
  • "the board is represented by a 64-bit number" -- I don't understand that. I mean, I can see how such a bitboard could be an aspect of such a representation. But as it stands, there just aren't enough bits, the best you could do is represent non-vacant vs vacant. Commented Dec 12, 2022 at 2:09
  • What is from __future__ import annotations all about? Or more specifically, what cPython interpreter version are you targetting? Modern interpreters should not need that declaration. Commented Dec 12, 2022 at 2:11
  • What is this assignment all about? self.__board: int = board That is, why are we venturing into double dunder danger, when a single underscore _board would suffice. Commented Dec 12, 2022 at 2:13
  • Bitboards are actually a staple of recent chess engines, I wont go into the nitty-gritty details but the purpose of them is to store as little data as possible. Instead of an array (which means storing 64 integers to represent the whole board), we can use as little as 12 integers (for each piece type, and for each color). here is a reference video Commented Dec 12, 2022 at 2:13
  • the future import is used so that I could type hint a class inside that class. again, here is a reference Commented Dec 12, 2022 at 2:14

1 Answer 1

1

I think you almost have it.

(And I'm sad that python typing isn't quite there, yet. I've been routinely using it in signatures for years, now, I hadn't realized there are still some pretty large rough edges.)

I am running mypy 0.990, with Python 3.10.8.


Randomly exercising the primitives looks like it works out just fine.

def manipulate_bitboard_with_literals():
    b = BitBoard(0x3)
    b.set_bit(4)  # set a high bit
    b = b & 0x3  # clear that bit
    b = b ^ 0x3  # back to zero
    b = b | 0x2
    assert b.board == 2


def combine_bitboards():
    a = BitBoard(0x3)
    b = BitBoard(0x8)
    c = a | b
    assert c.board == 11
    c &= ~0x2  # now it's 9
    c &= a
    assert c.board == 1

Here's what I changed to get that to work.

    def __or__(self, value: int | BitBoard) -> BitBoard:
        if isinstance(value, BitBoard):
            return BitBoard((self.__board | value.board) & FULL_MASK)
        return BitBoard((self.__board | value) & FULL_MASK)

I just coded that up without looking at anything, it seemed natural. I saw a | b produce the diagnostic

TypeError: unsupported operand type(s) for |: 'int' and 'BitBoard'

so I wrote what felt like a straightforward response to that.

Sorry if there's a DRY aspect to that which feels too verbose. I would love to see an improvement on it.


And then I tackled AND, referring to your code this time. It was nearly perfect, I think there was just a minor capitalization typo in "Bitboard" vs "BitBoard".

    def __and__(self, value: int | BitBoard) -> BitBoard:
        result: BitBoard

        if type(value) == int:
            result = BitBoard((self.__board & value) & FULL_MASK)
        if type(value) == BitBoard:
            result = BitBoard((self.__board & value.board) & FULL_MASK)

        return result

It's saying the same thing as the OR function, just a slightly different phrasing.

Imagine that another type, like None, found its way into that function. From the perspective of proving type safety, it seems like such a case is not yet handled by the above implementation, given that python's not doing runtime checking.

Overall, your class is in good shape. LGTM, ship it!


On reflection, I think I find this form preferable. It is concise, and there's just a single | OR operator.

The "give me an int!" expression could be extracted into a helper if desired.

    def __or__(self, value: int | BitBoard) -> BitBoard:
        value = value.board if isinstance(value, BitBoard) else value
        return BitBoard((self.__board | value) & FULL_MASK)
Sign up to request clarification or add additional context in comments.

1 Comment

Putting most of this in a helper, and passing in a function parameter, seems a promising route: docs.python.org/3/library/operator.html#operator.or_

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.