diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py new file mode 100644 index 0000000..c570d1b --- /dev/null +++ b/binding/python_c_ffi.py @@ -0,0 +1,190 @@ +import ctypes as C + + +class Board(C.Structure): + _fields_ = [ + ("pieces", C.c_uint64 * 12), + ("occ", C.c_uint64 * 3), + ("king_square", C.c_uint64 * 2), + ("castling_rights", C.c_uint8), + ("ep_square", C.c_int), + ("side_to_move", C.c_int), + ("halfmove_clock", C.c_int), + ("fullmove_number", C.c_int), + ] + + +class Move(C.Structure): + _fields_ = [ + ("from", C.c_uint16), + ("to", C.c_uint16), + ("piece", C.c_uint8), + ("promo", C.c_uint8), + ("flags", C.c_uint8), + ] + + +FILES = {c:i for i,c in enumerate("abcdefgh")} +WHITE, BLACK, BOTH = 0, 1, 2 +P, N, B, R, Q, K, p, n, b, r, q, k = range(12) + +""" + Register C function bindings and interfaces in a dictionary. +""" +PAWN_MOVE_SIG = ((C.POINTER(Board), C.POINTER(Move), C.POINTER(C.c_int)), None) +PIECE_MOVE_SIG = ((C.POINTER(Board), C.POINTER(Move), C.POINTER(C.c_int), C.c_bool), None) + +FFI_SPEC = { + "create_knight_attack_cache": None, + "create_king_attack_cache": None, + "create_pawn_attack_cache": None, + "gen_white_pawn_quiet_pushes": PAWN_MOVE_SIG, + "gen_black_pawn_quiet_pushes": PAWN_MOVE_SIG, + "gen_white_pawn_push_promotions": PAWN_MOVE_SIG, + "gen_black_pawn_push_promotions": PAWN_MOVE_SIG, + "gen_white_pawn_captures": PAWN_MOVE_SIG, + "gen_black_pawn_captures": PAWN_MOVE_SIG, + "gen_white_pawn_capture_promotions": PAWN_MOVE_SIG, + "gen_black_pawn_capture_promotions": PAWN_MOVE_SIG, + "gen_knight_moves": PIECE_MOVE_SIG, + "gen_bishop_moves": PIECE_MOVE_SIG, + "gen_rook_moves": PIECE_MOVE_SIG, + "gen_queen_moves": PIECE_MOVE_SIG, + "gen_king_moves": PIECE_MOVE_SIG, + "square_attacked": ((C.POINTER(Board), C.c_int, C.c_int), C.c_bool), + "in_check": ((C.POINTER(Board), C.c_int), C.c_bool), + "attackers_to": ((C.POINTER(Board), C.c_int, C.c_int), C.c_uint64), + "get_legal_moves":((C.POINTER(Board), C.POINTER(Move)), C.c_int), + "perft": ((C.POINTER(Board), C.c_int), C.c_uint64), + "load_fen": ((C.POINTER(Board), C.c_char_p), C.c_int), + "gen_pseudo_moves": ((C.POINTER(Board), C.POINTER(Move), C.c_bool), C.c_int), +} + + +class ChessFFI: + def __init__(self): + self.lib_path = "./build/lichess.so" + self._load_lib() + self._load_constants() + self._bind_functions() + + + def init_attack_cache(self): + self._c_create_knight_attack_cache() + self._c_create_king_attack_cache() + self._c_create_pawn_attack_cache() + + + def load_fen(self, board, fen_string): + return self._c_load_fen(C.byref(board), fen_string.encode("ascii")) + + + def gen_pseudo_moves(self, board, captures_only=False, cap=256): + buf = (Move * cap)() + n = self._c_gen_pseudo_moves(C.byref(board), buf, captures_only) + return buf, n + + + def get_legal_moves(self, board, out): + return int(self._c_get_legal_moves(C.byref(board), out)) + + + def square_attacked(self, board, sq, by): + return bool(self._c_square_attacked(C.byref(board), int(sq), int(by))) + + + def in_check(self, board, side): + return bool(self._c_in_check(C.byref(board), int(side))) + + + def attackers_to(self, board, sq, by): + return int(self._c_attackers_to(C.byref(board), int(sq), int(by))) + + + def sq_to_coord(self, sq): + return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8)) + + + def sq(self, name): + f = FILES[name[0].lower()] + r = int(name[1]) - 1 + return f + 8 * r + + + def popcount(self, x): + return x.bit_count() + + + def draw_bb(self, mask, origin=None): + print("\n") + lines = [] + for r in range(7, -1, -1): + row = [] + for f in range(8): + sqi = r * 8 + f + bit = (mask >> sqi) & 1 + if origin is not None and sqi == origin: + ch = 'O' + elif bit: + ch = 'x' + else: + ch = '.' + row.append(ch) + lines.append(f"{r+1} " + " ".join(row)) + lines.append(" " + " ".join(FILES)) + lines = "\n".join(lines) + print(lines, "\n") + + + def _load_lib(self): + self._lib = C.CDLL(self.lib_path) + + + def _load_constants(self): + piece_array = (C.c_uint64 * 64) + + # Account for both sets of pawn colors because + # they have different attack patterns. + pawn_array = piece_array * 2 + + self.KNIGHT_ATTACKS = self._in_dll_or_none("KNIGHT_ATTACKS", piece_array) + self.KING_ATTACKS = self._in_dll_or_none("KING_ATTACKS", piece_array) + self.PAWN_ATTACKS = self._in_dll_or_none("PAWN_ATTACKS", pawn_array) + + + def _bind_functions(self): + for name, spec in FFI_SPEC.items(): + if spec is None: + argtypes, restype = (), None + else: + argtypes, restype = spec + fn = getattr(self._lib, name, None) + if not fn: + print(f"missing function {name}") + + if fn is not None: + fn.argtypes = argtypes + fn.restype = restype + + # Prepend each function name with _c to allow us to reuse the + # function names as methods on this class. This means that we + # can use this class as interface to the C functions without + # modifying the names too much. + # + # Example: + # + # def load_fen(self): + # self._c_load_fen() + # + # ChessFFI.load_fen() + # + # + setattr(self, f"_c_{name}", fn) + + + def _in_dll_or_none(self, symbol, ctype): + try: + return ctype.in_dll(self._lib, symbol) + except ValueError: + print("Constant not found.") + return None \ No newline at end of file