1

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.

Alden Luthfi
  • 105
  • 9
  • "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. – J_H Dec 12 '22 at 02: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. – J_H Dec 12 '22 at 02: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. – J_H Dec 12 '22 at 02: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](https://www.youtube.com/watch?v=n5vV4u-nKtw) – Alden Luthfi Dec 12 '22 at 02:13
  • the __future__ import is used so that I could type hint a class inside that class. again, here is a [reference](https://stackoverflow.com/questions/41135033/type-hinting-within-a-class) – Alden Luthfi Dec 12 '22 at 02:14
  • The way I choose to name my variables might be subject to change, but as of now it is irrelevant to my question – Alden Luthfi Dec 12 '22 at 02:16
  • The `pop_bit` identifier seems weird, given that it returns `None`. Consider using `clear_bit` as the more natural identifier. (And thanks for the "modern engines" overview, that makes sense, you are using _several_ bitboards to represent the whole board, one facet at a time, cool.) – J_H Dec 12 '22 at 02:16
  • Ok, I finally got down to the "attempts" part. Sorry, I'm not getting it. It looks like "check for int and assign result" was pasted in a 2nd time? Also, since I can't see your pasted diagnostic message, I guess I'm gonna have to run `mypy` myself? – J_H Dec 12 '22 at 02:21
  • Yes, the typo is fixed now – Alden Luthfi Dec 12 '22 at 02:23
  • @J_H: `from __future__ import annotations` is *still* not on by default (and may never be, there have been issues with its design; they've pushed back the on-by-default version twice, and now it's just "TBD"). You need it even in 3.11. – ShadowRanger Dec 12 '22 at 02:34

1 Answers1

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)
J_H
  • 17,926
  • 4
  • 24
  • 44
  • Putting most of this in a helper, and passing in a function parameter, seems a promising route: https://docs.python.org/3/library/operator.html#operator.or_ – J_H Dec 12 '22 at 05:13