From f3b6b7448ea517762cae9fad02d5ac1532f61f2d Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 13:55:43 -0400 Subject: [PATCH 01/10] Add python c binding class interface --- binding/python_c_ffi.py | 190 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 binding/python_c_ffi.py 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 -- 2.34.1 From 5ddb3596751ddf215010416f78413988d05e228a Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 14:28:48 -0400 Subject: [PATCH 02/10] Fix attack to tests --- binding/python_c_ffi.py | 72 ++++++++++++++++++++--------------------- test/base.py | 8 +++-- test/test_attack_to.py | 26 ++++++--------- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py index c570d1b..0394b37 100644 --- a/binding/python_c_ffi.py +++ b/binding/python_c_ffi.py @@ -28,12 +28,12 @@ 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, @@ -61,9 +61,40 @@ FFI_SPEC = { } +def draw_bb(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 sq_to_coord(sq): + return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8)) + + +def sq(name): + f = FILES[name[0].lower()] + r = int(name[1]) - 1 + return f + 8 * r + + class ChessFFI: def __init__(self): - self.lib_path = "./build/lichess.so" + self.lib_path = "./build/libchess.so" self._load_lib() self._load_constants() self._bind_functions() @@ -76,7 +107,7 @@ class ChessFFI: def load_fen(self, board, fen_string): - return self._c_load_fen(C.byref(board), fen_string.encode("ascii")) + return self._c_load_fen(board, fen_string.encode("ascii")) def gen_pseudo_moves(self, board, captures_only=False, cap=256): @@ -98,44 +129,13 @@ class ChessFFI: 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 + return self._c_attackers_to(board, sq, by) 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) @@ -164,7 +164,7 @@ class ChessFFI: if fn is not None: fn.argtypes = argtypes - fn.restype = restype + 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 diff --git a/test/base.py b/test/base.py index c8143cd..9815980 100644 --- a/test/base.py +++ b/test/base.py @@ -1,4 +1,5 @@ import unittest +from binding.python_c_ffi import ChessFFI from test.chess_ffi import Board from test.chess_ffi import KING_ATTACKS from test.chess_ffi import KNIGHT_ATTACKS @@ -11,6 +12,9 @@ from test.chess_ffi import load_fen class ChessLibTestBase(unittest.TestCase): @classmethod def setUpClass(cls): + cls.chess_ffi = ChessFFI() + + init_attack_caches() cls.KNIGHT_ATTACKS = KNIGHT_ATTACKS @@ -25,8 +29,8 @@ class ChessLibTestBase(unittest.TestCase): def load_fen(self, fen, board=None): if board: - return load_fen(board, fen) - return load_fen(self.board, fen) + return self.chess_ffi.load_fen(board, fen) + return self.chess_ffi.load_fen(self.board, fen) def gen(self, captures_only: bool = False, cap: int = 256): diff --git a/test/test_attack_to.py b/test/test_attack_to.py index cc099d3..9633c8a 100644 --- a/test/test_attack_to.py +++ b/test/test_attack_to.py @@ -1,13 +1,8 @@ -import ctypes +from binding.python_c_ffi import Board +from binding.python_c_ffi import sq +from binding.python_c_ffi import BLACK +from binding.python_c_ffi import WHITE from test.base import ChessLibTestBase -from test.chess_ffi import get_attackers_to -from test.chess_ffi import is_square_attacked -from test.chess_ffi import is_in_check -from test.chess_ffi import sq -from test.chess_ffi import Board -from test.chess_ffi import BLACK -from test.chess_ffi import WHITE -from test.chess_ffi import draw_bb class TestAttackers(ChessLibTestBase): @@ -50,8 +45,8 @@ class TestAttackers(ChessLibTestBase): for fen, sq_str, by, expected, msg in cases: with self.subTest(msg=msg, fen=fen, sq=sq_str, by=by): b = Board() - self.load_fen(fen, board=b) - got = bool(is_square_attacked(b, sq(sq_str), by)) + self.chess_ffi.load_fen(b, fen) + got = self.chess_ffi.square_attacked(b, sq(sq_str), by) self.assertEqual(expected, got, msg) @@ -81,8 +76,8 @@ class TestAttackers(ChessLibTestBase): for fen, side, expected, msg in cases: with self.subTest(msg=msg, fen=fen, side=side): b = Board() - self.load_fen(fen, board=b) - actual = bool(is_in_check(b, side)) + self.chess_ffi.load_fen(b, fen) + actual = self.chess_ffi.in_check(b, side) self.assertEqual(expected, actual, msg) @@ -132,7 +127,6 @@ class TestAttackers(ChessLibTestBase): for fen, sq_str, by, expected_cnt, msg in cases: with self.subTest(msg=msg, fen=fen, sq=sq_str, by=by): b = Board() - self.load_fen(fen, board=b) - - mask = get_attackers_to(b, sq(sq_str), by) + self.chess_ffi.load_fen(b, fen) + mask = self.chess_ffi.attackers_to(b, sq(sq_str), by) self.assertEqual(expected_cnt, int(mask).bit_count()) \ No newline at end of file -- 2.34.1 From 2a7948eab4b58c0e80889343ed9449080bac9a56 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 14:47:19 -0400 Subject: [PATCH 03/10] Fix fen loading tests --- binding/python_c_ffi.py | 12 ++++--- makefile | 2 +- test/base.py | 28 ++------------- test/test_fen_loader.py | 79 ++++++++++++++++++++--------------------- 4 files changed, 51 insertions(+), 70 deletions(-) diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py index 0394b37..dcc1dc8 100644 --- a/binding/python_c_ffi.py +++ b/binding/python_c_ffi.py @@ -92,6 +92,14 @@ def sq(name): return f + 8 * r +def popcount(x): + return x.bit_count() + + +def get_rank_mask(rank): + return sum(1 << (rank*8 + f) for f in range(8)) + + class ChessFFI: def __init__(self): self.lib_path = "./build/libchess.so" @@ -130,10 +138,6 @@ class ChessFFI: def attackers_to(self, board, sq, by): return self._c_attackers_to(board, sq, by) - - - def popcount(self, x): - return x.bit_count() def _load_lib(self): diff --git a/makefile b/makefile index 4c0d37e..3e21820 100644 --- a/makefile +++ b/makefile @@ -55,4 +55,4 @@ test: @python3 -m unittest -v; \ status=$$?; \ $(MAKE) clean; \ - exit $$status + exit $$status \ No newline at end of file diff --git a/test/base.py b/test/base.py index 9815980..496e6a4 100644 --- a/test/base.py +++ b/test/base.py @@ -1,37 +1,15 @@ import unittest +from binding.python_c_ffi import Board from binding.python_c_ffi import ChessFFI -from test.chess_ffi import Board -from test.chess_ffi import KING_ATTACKS -from test.chess_ffi import KNIGHT_ATTACKS -from test.chess_ffi import PAWN_ATTACKS -from test.chess_ffi import gen_moves -from test.chess_ffi import init_attack_caches -from test.chess_ffi import load_fen class ChessLibTestBase(unittest.TestCase): @classmethod def setUpClass(cls): cls.chess_ffi = ChessFFI() - - - init_attack_caches() - - cls.KNIGHT_ATTACKS = KNIGHT_ATTACKS - cls.KING_ATTACKS = KING_ATTACKS - cls.PAWN_ATTACKS = PAWN_ATTACKS + cls.chess_ffi.init_attack_cache() def setUp(self): # This should be an empty board for each test in the suite. - self.board = Board() - - - def load_fen(self, fen, board=None): - if board: - return self.chess_ffi.load_fen(board, fen) - return self.chess_ffi.load_fen(self.board, fen) - - - def gen(self, captures_only: bool = False, cap: int = 256): - return gen_moves(self.board, captures_only=captures_only, cap=cap) \ No newline at end of file + self.board = Board() \ No newline at end of file diff --git a/test/test_fen_loader.py b/test/test_fen_loader.py index 37620b0..e290c61 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -1,65 +1,64 @@ from test.base import ChessLibTestBase -from test.chess_ffi import Board -from test.chess_ffi import BLACK -from test.chess_ffi import WHITE -from test.chess_ffi import sq -from test.chess_ffi import popcount -from test.chess_ffi import load_fen +from binding.python_c_ffi import Board +from binding.python_c_ffi import BLACK +from binding.python_c_ffi import WHITE +from binding.python_c_ffi import sq +from binding.python_c_ffi import popcount +from binding.python_c_ffi import get_rank_mask class TestFenLoading(ChessLibTestBase): - - - def rank_mask(self, r): - return sum(1 << (r*8 + f) for f in range(8)) - + def test_startpos_fields_and_occupancies(self): + b = Board() fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" - rc = self.load_fen(fen) + rc = self.chess_ffi.load_fen(b, fen) self.assertEqual(rc, 0) - self.assertEqual(self.board.side_to_move, WHITE) - self.assertEqual(self.board.castling_rights, 0b1111) - self.assertEqual(self.board.ep_square, -1) - self.assertEqual(self.board.halfmove_clock, 0) - self.assertEqual(self.board.fullmove_number, 1) + self.assertEqual(b.side_to_move, WHITE) + self.assertEqual(b.castling_rights, 0b1111) + self.assertEqual(b.ep_square, -1) + self.assertEqual(b.halfmove_clock, 0) + self.assertEqual(b.fullmove_number, 1) - white_expected = self.rank_mask(0) | self.rank_mask(1) - black_expected = self.rank_mask(6) | self.rank_mask(7) - self.assertEqual(int(self.board.occ[WHITE]), white_expected) - self.assertEqual(int(self.board.occ[BLACK]), black_expected) - self.assertEqual(int(self.board.occ[2]), white_expected | black_expected) - - self.assertEqual(popcount(int(self.board.occ[WHITE])), 16) - self.assertEqual(popcount(int(self.board.occ[BLACK])), 16) + white_expected = get_rank_mask(0) | get_rank_mask(1) + black_expected = get_rank_mask(6) | get_rank_mask(7) + + self.assertEqual(int(b.occ[WHITE]), white_expected) + self.assertEqual(int(b.occ[BLACK]), black_expected) + self.assertEqual(int(b.occ[2]), white_expected | black_expected) + self.assertEqual(popcount(int(b.occ[WHITE])), 16) + self.assertEqual(popcount(int(b.occ[BLACK])), 16) def test_castling_flags_parsing(self): + b = Board() fen = "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 5 42" - - rc = self.load_fen(fen) - self.assertEqual(self.board.side_to_move, BLACK) - self.assertEqual(self.board.castling_rights, 0b1111) - self.assertEqual(self.board.halfmove_clock, 5) - self.assertEqual(self.board.fullmove_number, 42) + self.chess_ffi.load_fen(b, fen) + + self.assertEqual(b.side_to_move, BLACK) + self.assertEqual(b.castling_rights, 0b1111) + self.assertEqual(b.halfmove_clock, 5) + self.assertEqual(b.fullmove_number, 42) def test_en_passant_targets(self): # EP at e6, black to move + b1 = Board() fen = "8/8/8/3pP3/8/8/8/8 b KQkq e6 0 1" - rc = self.load_fen(fen) + rc = self.chess_ffi.load_fen(b1, fen) self.assertEqual(rc, 0) - self.assertEqual(self.board.side_to_move, BLACK) - self.assertEqual(self.board.ep_square, sq("e6")) + self.assertEqual(b1.side_to_move, BLACK) + self.assertEqual(b1.ep_square, sq("e6")) # EP at d3, white to move — use a fresh Board + b2 = Board() fen = "8/8/8/8/3Pp3/8/8/8 w KQkq d3 12 7" - b = Board() - rc = self.load_fen(fen, board=b) + rc = self.chess_ffi.load_fen(b2, fen) self.assertEqual(rc, 0) - self.assertEqual(b.side_to_move, WHITE) - self.assertEqual(b.ep_square, sq("d3")) + self.assertEqual(b2.side_to_move, WHITE) + self.assertEqual(b2.ep_square, sq("d3")) def test_malformed_piece_field(self): @@ -70,7 +69,7 @@ class TestFenLoading(ChessLibTestBase): "8/8/8/8/8/8/8/8w - - 0 1", ] for fen in bad: - r = self.load_fen(fen, board=Board()) + r = self.chess_ffi.load_fen(Board(), fen) self.assertEqual(r, -1) @@ -82,5 +81,5 @@ class TestFenLoading(ChessLibTestBase): "8/8/8/8/8/8/8/8 w - e4 0 1", # EP not rank 3/6 (your code allows any 1..8; consider tightening) ] for fen in bad: - r = self.load_fen(fen, board=Board()) + r = self.chess_ffi.load_fen(Board(), fen) self.assertEqual(r, -1) \ No newline at end of file -- 2.34.1 From 616c4569649979c9e6fca0e35861d8988dec7762 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 14:53:10 -0400 Subject: [PATCH 04/10] Fix attack cache tests --- binding/python_c_ffi.py | 7 +++++++ test/test_piece_attack_cache.py | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py index dcc1dc8..e34c635 100644 --- a/binding/python_c_ffi.py +++ b/binding/python_c_ffi.py @@ -100,6 +100,13 @@ def get_rank_mask(rank): return sum(1 << (rank*8 + f) for f in range(8)) +def bb_from(*algebraic): + m = 0 + for s in algebraic: + m |= (1 << sq(s)) + return m + + class ChessFFI: def __init__(self): self.lib_path = "./build/libchess.so" diff --git a/test/test_piece_attack_cache.py b/test/test_piece_attack_cache.py index 25ad432..be7e085 100644 --- a/test/test_piece_attack_cache.py +++ b/test/test_piece_attack_cache.py @@ -1,75 +1,75 @@ from test.base import ChessLibTestBase -from test.chess_ffi import bb_from -from test.chess_ffi import draw_bb -from test.chess_ffi import sq -from test.chess_ffi import BLACK, WHITE +from binding.python_c_ffi import bb_from +from binding.python_c_ffi import sq +from binding.python_c_ffi import BLACK +from binding.python_c_ffi import WHITE class KnightFixedCases(ChessLibTestBase): def test_knight_a1(self): expected = bb_from("b3", "c2") - self.assertEqual(int(self.KNIGHT_ATTACKS[sq("a1")]), expected) + self.assertEqual(int(self.chess_ffi.KNIGHT_ATTACKS[sq("a1")]), expected) def test_knight_d4(self): expected = bb_from("b5","b3","c6","e6","f5","f3","c2","e2") - actual = int(self.KNIGHT_ATTACKS[sq("d4")]) + actual = int(self.chess_ffi.KNIGHT_ATTACKS[sq("d4")]) self.assertEqual(actual, expected) def test_knight_h8(self): expected = bb_from("f7","g6") - actual = int(self.KNIGHT_ATTACKS[sq("h8")]) + actual = int(self.chess_ffi.KNIGHT_ATTACKS[sq("h8")]) self.assertEqual(actual, expected) class PawnFixedCases(ChessLibTestBase): def test_white_pawn_a2(self): expected = bb_from("b3") - actual = int(self.PAWN_ATTACKS[WHITE][sq("a2")]) + actual = int(self.chess_ffi.PAWN_ATTACKS[WHITE][sq("a2")]) self.assertEqual(actual, expected) def test_white_pawn_b2(self): expected = bb_from("a3", "c3") - actual = int(self.PAWN_ATTACKS[WHITE][sq("b2")]) + actual = int(self.chess_ffi.PAWN_ATTACKS[WHITE][sq("b2")]) self.assertEqual(actual, expected) def test_black_pawn_a2(self): expected = bb_from("b6") - actual = int(self.PAWN_ATTACKS[BLACK][sq("a7")]) + actual = int(self.chess_ffi.PAWN_ATTACKS[BLACK][sq("a7")]) self.assertEqual(actual, expected) def test_black_pawn_b2(self): expected = bb_from("a6", "c6") - actual = int(self.PAWN_ATTACKS[BLACK][sq("b7")]) + actual = int(self.chess_ffi.PAWN_ATTACKS[BLACK][sq("b7")]) self.assertEqual(actual, expected) class KingFixedCases(ChessLibTestBase): def test_king_a1(self): expected = bb_from("a2", "b1", "b2") - actual = int(self.KING_ATTACKS[sq("a1")]) + actual = int(self.chess_ffi.KING_ATTACKS[sq("a1")]) self.assertEqual(actual, expected) def test_king_a8(self): expected = bb_from("a7", "b8", "b7") - actual = int(self.KING_ATTACKS[sq("a8")]) + actual = int(self.chess_ffi.KING_ATTACKS[sq("a8")]) self.assertEqual(actual, expected) def test_king_h1(self): expected = bb_from("h2", "g1", "g2") - actual = int(self.KING_ATTACKS[sq("h1")]) + actual = int(self.chess_ffi.KING_ATTACKS[sq("h1")]) self.assertEqual(actual, expected) def test_king_h8(self): expected = bb_from("h7", "g7", "g8") - actual = int(self.KING_ATTACKS[sq("h8")]) + actual = int(self.chess_ffi.KING_ATTACKS[sq("h8")]) self.assertEqual(actual, expected) @@ -79,5 +79,5 @@ class KingFixedCases(ChessLibTestBase): "c4", "e4", "c5", "d5", "e5", ) - actual = int(self.KING_ATTACKS[sq("d4")]) + actual = int(self.chess_ffi.KING_ATTACKS[sq("d4")]) self.assertEqual(actual, expected) \ No newline at end of file -- 2.34.1 From 8538591d49c5457df439918e08f42f0e4f9fe77e Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:09:48 -0400 Subject: [PATCH 05/10] Fix legal move gen --- binding/python_c_ffi.py | 2 +- test/test_legal_move_gen.py | 52 +++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py index e34c635..0f32325 100644 --- a/binding/python_c_ffi.py +++ b/binding/python_c_ffi.py @@ -132,7 +132,7 @@ class ChessFFI: def get_legal_moves(self, board, out): - return int(self._c_get_legal_moves(C.byref(board), out)) + return int(self._c_get_legal_moves(board, out)) def square_attacked(self, board, sq, by): diff --git a/test/test_legal_move_gen.py b/test/test_legal_move_gen.py index 88073bb..907767d 100644 --- a/test/test_legal_move_gen.py +++ b/test/test_legal_move_gen.py @@ -1,19 +1,17 @@ from test.base import ChessLibTestBase -from test.chess_ffi import Move -from test.chess_ffi import Board -from test.chess_ffi import BLACK -from test.chess_ffi import is_in_check -from test.chess_ffi import gen_legal_moves -from test.chess_ffi import sq +from binding.python_c_ffi import Board +from binding.python_c_ffi import Move +from binding.python_c_ffi import BLACK +from binding.python_c_ffi import sq + MAX_MOVES = 256 class TestLegalMoveGen(ChessLibTestBase): def _gen_legal(self, board): - """Support either return-count or out-parameter signatures.""" moves = (Move * MAX_MOVES)() - n = gen_legal_moves(board, moves) + n = self.chess_ffi.get_legal_moves(board, moves) return n, moves @@ -22,7 +20,7 @@ class TestLegalMoveGen(ChessLibTestBase): # https://www.chessprogramming.org/Perft_Results#Initial_Position fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" b = Board() - self.load_fen(fen, board=b) + self.chess_ffi.load_fen(b, fen) n, _ = self._gen_legal(b) self.assertEqual(n, 20, "Start position must have 20 legal moves for White") @@ -32,7 +30,8 @@ class TestLegalMoveGen(ChessLibTestBase): # Kiwipete: perft(1) = 48 # https://www.chessprogramming.org/Perft_Results#Position_2 fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" - b = Board(); self.load_fen(fen, board=b) + b = Board() + self.chess_ffi.load_fen(b, fen) n, moves = self._gen_legal(b) self.assertEqual(n, 48, "Kiwipete perft(1) should be 48") @@ -41,7 +40,8 @@ class TestLegalMoveGen(ChessLibTestBase): def test_perft_position_3(self): # https://www.chessprogramming.org/Perft_Results#Position_3 fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1" - b = Board(); self.load_fen(fen, board=b) + b = Board() + self.chess_ffi.load_fen(b, fen) n, moves = self._gen_legal(b) self.assertEqual(n, 14) @@ -50,7 +50,8 @@ class TestLegalMoveGen(ChessLibTestBase): def test_perft_position_4(self): # https://www.chessprogramming.org/Perft_Results#Position_4 fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" - b = Board(); self.load_fen(fen, board=b) + b = Board() + self.chess_ffi.load_fen(b, fen) n, moves = self._gen_legal(b) self.assertEqual(n, 6) @@ -59,7 +60,8 @@ class TestLegalMoveGen(ChessLibTestBase): def test_perft_position_5(self): # https://www.chessprogramming.org/Perft_Results#Position_5 fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" - b = Board(); self.load_fen(fen, board=b) + b = Board() + self.chess_ffi.load_fen(b, fen) n, moves = self._gen_legal(b) self.assertEqual(n, 44) @@ -68,7 +70,8 @@ class TestLegalMoveGen(ChessLibTestBase): def test_perft_position_6(self): # https://www.chessprogramming.org/Perft_Results#Position_6 fen = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" - b = Board(); self.load_fen(fen, board=b) + b = Board() + self.chess_ffi.load_fen(b, fen) n, moves = self._gen_legal(b) self.assertEqual(n, 46) @@ -78,32 +81,31 @@ class TestLegalMoveGen(ChessLibTestBase): # Classic stalemate: Black to move, no legal moves, not in check fen = "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1" b = Board() - self.load_fen(fen, board=b) + self.chess_ffi.load_fen(b, fen) - moves = (Move * MAX_MOVES)() - n = gen_legal_moves(b, moves) + n, moves = self._gen_legal(b) self.assertEqual(n, 0, "Stalemate should have 0 legal moves") - self.assertFalse(is_in_check(b, BLACK), "Side to move must not be in check in stalemate") + self.assertFalse(self.chess_ffi.in_check(b, BLACK), "Side to move must not be in check in stalemate") def test_simple_checkmate(self): # Black to move; mated (Qg7# with Kg6 support) fen = "7k/6Q1/6K1/8/8/8/8/8 b - - 0 1" - b = Board(); self.load_fen(fen, board=b) - moves = (Move * MAX_MOVES)() - n = gen_legal_moves(b, moves) + b = Board() + self.chess_ffi.load_fen(b, fen) + n, moves = self._gen_legal(b) self.assertEqual(n, 0, "Checkmated side should have 0 legal moves") - self.assertTrue(is_in_check(b, BLACK), "Mated side must be in check") + self.assertTrue(self.chess_ffi.in_check(b, BLACK), "Mated side must be in check") def test_pinned_piece_cannot_move(self): # White knight e2 pinned by rook e8 vs king e1 fen = "4r3/8/8/8/8/8/4N3/4K3 w - - 0 1" - b = Board(); self.load_fen(fen, board=b) - moves = (Move * MAX_MOVES)() - n = gen_legal_moves(b, moves) + b = Board() + self.chess_ffi.load_fen(b, fen) + n, moves = self._gen_legal(b) for move in moves: # Pinned knight cannot move from it's square. -- 2.34.1 From 1023199e843a890aa1d08379f3b548e045fc316c Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:16:32 -0400 Subject: [PATCH 06/10] Fix move gen tests --- test/test_movegen.py | 54 +++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/test/test_movegen.py b/test/test_movegen.py index 0850531..0c985e3 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -1,18 +1,7 @@ import ctypes +from binding.python_c_ffi import Board +from binding.python_c_ffi import Move from test.base import ChessLibTestBase -from test.chess_ffi import gen_white_pawn_quiet_pushes -from test.chess_ffi import gen_white_pawn_push_promotions -from test.chess_ffi import gen_white_pawn_capture_promotions -from test.chess_ffi import gen_white_pawn_captures -from test.chess_ffi import gen_knight_moves -from test.chess_ffi import gen_bishop_moves -from test.chess_ffi import gen_rook_moves -from test.chess_ffi import gen_queen_moves -from test.chess_ffi import gen_king_moves -from test.chess_ffi import Move -from test.chess_ffi import Board -from test.chess_ffi import BLACK -from test.chess_ffi import WHITE MAX_MOVES = 256 @@ -26,12 +15,12 @@ class TestPseudoMoveGeneration(ChessLibTestBase): moves = (Move * MAX_MOVES)() b = Board() - self.load_fen(fen, board=b) + self.chess_ffi.load_fen(b , fen) if captures_only is not None: - move_function(b, moves, ctypes.byref(cnt), captures_only) + move_function(b, moves, cnt, captures_only) else: # Account for pawn move gen. - move_function(b, moves, ctypes.byref(cnt)) + move_function(b, moves, cnt) self.assertEqual(expected, cnt.value) @@ -61,8 +50,8 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("8/8/8/8/3N4/1P3p2/4P3/8 w - - 0 1", 1, "d4 with f3 black (1 cap), b3/e2 white (not capturable)"), ("8/8/2p1p3/1p3p2/3N4/1p3p2/2p1p3/8 w - - 0 1", 8, "d4 with all 8 targets black: 8 captures"), ] - self.run_subtests(all_move_types, gen_knight_moves, captures_only=False) - self.run_subtests(captures_only, gen_knight_moves, captures_only=True) + self.run_subtests(all_move_types, self.chess_ffi._c_gen_knight_moves, captures_only=False) + self.run_subtests(captures_only, self.chess_ffi._c_gen_knight_moves, captures_only=True) def test_bishop_move_gen(self): @@ -94,8 +83,8 @@ class TestPseudoMoveGeneration(ChessLibTestBase): # mixed: only two rays have a capture ("8/8/1p3P2/8/3B4/2P5/5p2/8 w - - 0 1", 2, "d4: NW capture b6, SE capture f2; NE/SW blocked by own"), ] - self.run_subtests(cases_bishop_all, gen_bishop_moves, captures_only=False) - self.run_subtests(cases_bishop_caps, gen_bishop_moves, captures_only=True) + self.run_subtests(cases_bishop_all, self.chess_ffi._c_gen_bishop_moves, captures_only=False) + self.run_subtests(cases_bishop_caps, self.chess_ffi._c_gen_bishop_moves, captures_only=True) def test_rook_move_gen(self): @@ -133,8 +122,8 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("8/8/8/3P4/p2RP3/8/8/3p4 w - - 0 1", 2, "own on d5,e4; captures on a4 and d1"), ] - self.run_subtests(cases_rook_all, gen_rook_moves, captures_only=False) - self.run_subtests(cases_rook_caps, gen_rook_moves, captures_only=True) + self.run_subtests(cases_rook_all, self.chess_ffi._c_gen_rook_moves, captures_only=False) + self.run_subtests(cases_rook_caps, self.chess_ffi._c_gen_rook_moves, captures_only=True) def test_queen_move_gen(self): @@ -173,8 +162,8 @@ class TestPseudoMoveGeneration(ChessLibTestBase): # mixed: captures on two diagonals only ("7p/p7/8/8/3Q4/8/8/p5p1 w - - 0 1", 4, "diagonal targets a1,a7,g1,h8 (4 captures)"), ] - self.run_subtests(cases_queen_all, gen_queen_moves, captures_only=False) - self.run_subtests(cases_queen_caps, gen_queen_moves, captures_only=True) + self.run_subtests(cases_queen_all, self.chess_ffi._c_gen_queen_moves, captures_only=False) + self.run_subtests(cases_queen_caps, self.chess_ffi._c_gen_queen_moves, captures_only=True) def test_king_move_gen(self): @@ -232,11 +221,10 @@ class TestPseudoMoveGeneration(ChessLibTestBase): # blocked queenside (d8 occupied) → only O-O available ("r2bk2r/8/8/8/8/8/8/8 b kq - 0 1", 5, "d8 blocked; only king-side castle"), ] - - self.run_subtests(cases_king_all, gen_king_moves, captures_only=False) - self.run_subtests(cases_king_castle_white, gen_king_moves, captures_only=False) - self.run_subtests(cases_king_caps, gen_king_moves, captures_only=True) - self.run_subtests(cases_king_castle_black, gen_king_moves, captures_only=False) + self.run_subtests(cases_king_all, self.chess_ffi._c_gen_king_moves, captures_only=False) + self.run_subtests(cases_king_castle_white, self.chess_ffi._c_gen_king_moves, captures_only=False) + self.run_subtests(cases_king_caps, self.chess_ffi._c_gen_king_moves, captures_only=True) + self.run_subtests(cases_king_castle_black, self.chess_ffi._c_gen_king_moves, captures_only=False) def test_quiet_pawn_pushes_white(self): @@ -248,7 +236,7 @@ class TestPseudoMoveGeneration(ChessLibTestBase): # Although legal move, we have a separate function that calculates this move type. ("8/9P/8/8/8/8/8/8 w - - 0 1", 0, "no promotion push"), ] - self.run_subtests(cases, gen_white_pawn_quiet_pushes) + self.run_subtests(cases, self.chess_ffi._c_gen_white_pawn_quiet_pushes) def test_quiet_pawn_promotions_white(self): @@ -258,7 +246,7 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("7n/7P/8/8/8/8/8/8 w - - 0 1", 0, "blocked by enemy"), ("7N/7P/8/8/8/8/8/8 w - - 0 1", 0, "blocked by friendly"), ] - self.run_subtests(cases, gen_white_pawn_push_promotions) + self.run_subtests(cases, self.chess_ffi._c_gen_white_pawn_push_promotions) def test_capture_pawn_promotions_white(self): @@ -267,7 +255,7 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("5n1n/6P1/8/8/8/8/8/8 w - - 0 1", 8, "two captures"), ("8/7P/8/8/8/8/8/8 w - - 0 1", 0, "no capture"), ] - self.run_subtests(cases, gen_white_pawn_capture_promotions) + self.run_subtests(cases, self.chess_ffi._c_gen_white_pawn_capture_promotions) def test_capture_pawn_white(self): @@ -283,4 +271,4 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("8/8/5n2/3pP3/8/8/8/8 w - d6 0 1", 2, "EP e5xd6 and normal e5xf6"), ("8/8/8/3p4/8/4P3/8/8 w - d6 0 1", 0, "EP square present but no white pawn can take"), ] - self.run_subtests(cases, gen_white_pawn_captures) \ No newline at end of file + self.run_subtests(cases, self.chess_ffi._c_gen_white_pawn_captures) \ No newline at end of file -- 2.34.1 From ed0f2f6df9b42917f285f48de479c664e404d960 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:21:30 -0400 Subject: [PATCH 07/10] Fix perft tests --- test/test_perft.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/test/test_perft.py b/test/test_perft.py index 55aa2e5..8887e1c 100644 --- a/test/test_perft.py +++ b/test/test_perft.py @@ -1,12 +1,12 @@ from test.base import ChessLibTestBase -from test.chess_ffi import Board -from test.chess_ffi import perft +from binding.python_c_ffi import Board class TestPerftQuick(ChessLibTestBase): def test_perft_1(self): b = Board() - self.load_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", board=b) + fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 20, 2: 400, @@ -15,13 +15,14 @@ class TestPerftQuick(ChessLibTestBase): 5: 4865609, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") def test_perft_2(self): b = Board() - self.load_fen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", board=b) + fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 48, 2: 2039, @@ -29,13 +30,14 @@ class TestPerftQuick(ChessLibTestBase): 4: 4085603, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") def test_perft_3(self): b = Board() - self.load_fen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", board=b) + fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 14, 2: 191, @@ -44,13 +46,14 @@ class TestPerftQuick(ChessLibTestBase): 5: 674624, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") def test_perft_4(self): b = Board() - self.load_fen("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", board=b) + fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 6, 2: 264, @@ -58,13 +61,14 @@ class TestPerftQuick(ChessLibTestBase): 4: 422333, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") def test_perft_5(self): b = Board() - self.load_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8 ", board=b) + fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 44, 2: 1486, @@ -72,13 +76,14 @@ class TestPerftQuick(ChessLibTestBase): 4: 2103487, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") def test_perft_6(self): b = Board() - self.load_fen("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10 ", board=b) + fen = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" + self.chess_ffi.load_fen(b, fen) node_count = { 1: 46, 2: 2079, @@ -86,5 +91,5 @@ class TestPerftQuick(ChessLibTestBase): 4: 3894594, } for node, count in node_count.items(): - actual = int(perft(b, node)) + actual = int(self.chess_ffi._c_perft(b, node)) self.assertEqual(count, actual, f"perft({node}) start pos") \ No newline at end of file -- 2.34.1 From 18a2f142cd66b36b787c30c8b7ba6efdfae0580f Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:23:45 -0400 Subject: [PATCH 08/10] Formatting --- test/test_attack_to.py | 2 +- test/test_fen_loader.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_attack_to.py b/test/test_attack_to.py index 9633c8a..cfed01d 100644 --- a/test/test_attack_to.py +++ b/test/test_attack_to.py @@ -1,8 +1,8 @@ +from test.base import ChessLibTestBase from binding.python_c_ffi import Board from binding.python_c_ffi import sq from binding.python_c_ffi import BLACK from binding.python_c_ffi import WHITE -from test.base import ChessLibTestBase class TestAttackers(ChessLibTestBase): diff --git a/test/test_fen_loader.py b/test/test_fen_loader.py index e290c61..908ced3 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -8,8 +8,6 @@ from binding.python_c_ffi import get_rank_mask class TestFenLoading(ChessLibTestBase): - - def test_startpos_fields_and_occupancies(self): b = Board() fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" -- 2.34.1 From dbb2b8ae4277c99925957ced8186f0b0405c4337 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:24:11 -0400 Subject: [PATCH 09/10] Remove old chess_ffi --- test/chess_ffi.py | 188 ---------------------------------------------- 1 file changed, 188 deletions(-) delete mode 100644 test/chess_ffi.py diff --git a/test/chess_ffi.py b/test/chess_ffi.py deleted file mode 100644 index 57b0ea5..0000000 --- a/test/chess_ffi.py +++ /dev/null @@ -1,188 +0,0 @@ -""" - FFI - Foreign Function Interface - - This module needs to reflect the function interfaces that are - defined in our C modules. This may or may not be a good way to - test C since we essentially need to maintain two sets of interfaces. -""" -import ctypes as C - - -def _lib_path(): - # Just use a relative path from makefile. - return "./build/libchess.so" - - -_lib = C.CDLL(str(_lib_path())) - - -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) - - -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), - ] - - -_lib.load_fen.argtypes = (C.POINTER(Board), C.c_char_p) -_lib.load_fen.restype = C.c_int - -_lib.gen_pseudo_moves.argtypes = (C.POINTER(Board), C.POINTER(Move), C.c_bool) -_lib.gen_pseudo_moves.restype = C.c_int - - -def _bind_opt(name, argtypes=(), restype=None): - fn = getattr(_lib, name, None) - if fn is not None: - fn.argtypes = argtypes - fn.restype = restype - return fn - - -create_knight_attack_cache = _bind_opt("create_knight_attack_cache", (), None) -create_king_attack_cache = _bind_opt("create_king_attack_cache", (), None) -create_pawn_attack_cache = _bind_opt("create_pawn_attack_cache", (), None) - - -# PAWN move generation. -PAWN_SIG = (C.POINTER(Board), C.POINTER(Move), C.POINTER(C.c_int)), None - -gen_white_pawn_quiet_pushes = _bind_opt("gen_white_pawn_quiet_pushes", *PAWN_SIG) -gen_black_pawn_quiet_pushes = _bind_opt("gen_black_pawn_quiet_pushes", *PAWN_SIG) -gen_white_pawn_push_promotions = _bind_opt("gen_white_pawn_push_promotions", *PAWN_SIG) -gen_black_pawn_push_promotions = _bind_opt("gen_black_pawn_push_promotions", *PAWN_SIG) -gen_white_pawn_captures = _bind_opt("gen_white_pawn_captures", *PAWN_SIG) -gen_black_pawn_captures = _bind_opt("gen_black_pawn_captures", *PAWN_SIG) -gen_white_pawn_capture_promotions = _bind_opt("gen_white_pawn_capture_promotions", *PAWN_SIG) -gen_black_pawn_capture_promotions = _bind_opt("gen_black_pawn_capture_promotions", *PAWN_SIG) - - -# Non pawn move generation. -PIECE_SIG = ((C.POINTER(Board), C.POINTER(Move), C.POINTER(C.c_int), C.c_bool), None) - -gen_knight_moves = _bind_opt("gen_knight_moves", *PIECE_SIG) -gen_bishop_moves = _bind_opt("gen_bishop_moves", *PIECE_SIG) -gen_rook_moves = _bind_opt("gen_rook_moves", *PIECE_SIG) -gen_queen_moves = _bind_opt("gen_queen_moves", *PIECE_SIG) -gen_king_moves = _bind_opt("gen_king_moves", *PIECE_SIG) - - -# Attack checks. -ATTACKED_SIG = (C.POINTER(Board), C.c_int, C.c_int) -INCHECK_ARGS = (C.POINTER(Board), C.c_int) -ATTACKERS_TO = (C.POINTER(Board), C.c_int, C.c_int) -GEN_LEGAL_MOVES = (C.POINTER(Board), C.POINTER(Move)) - -square_attacked = _bind_opt("square_attacked", ATTACKED_SIG, C.c_bool) -in_check = _bind_opt("in_check", INCHECK_ARGS, C.c_bool) -attackers_to = _bind_opt("attackers_to", ATTACKERS_TO, C.c_uint64) -get_legal_moves = _bind_opt("get_legal_moves", GEN_LEGAL_MOVES, C.c_int) - -PERFT_SIG = (C.POINTER(Board), C.c_int) -perft = _bind_opt("perft", PERFT_SIG, C.c_uint64) - -# Attack cache tables. -KnightArr = (C.c_uint64 * 64) -KingArr = (C.c_uint64 * 64) -PawnRow = (C.c_uint64 * 64) -PawnArr = PawnRow * 2 -try: - KNIGHT_ATTACKS = KnightArr.in_dll(_lib, "KNIGHT_ATTACKS") - KING_ATTACKS = KingArr.in_dll(_lib, "KING_ATTACKS") - PAWN_ATTACKS = PawnArr.in_dll(_lib, "PAWN_ATTACKS") -except ValueError: - KNIGHT_ATTACKS = KING_ATTACKS = PAWN_ATTACKS = None # symbols not exported - - -def init_attack_caches(): - if create_knight_attack_cache: create_knight_attack_cache() - if create_king_attack_cache: create_king_attack_cache() - if create_pawn_attack_cache: create_pawn_attack_cache() - - -def load_fen(board, fen): - return _lib.load_fen(C.byref(board), fen.encode("ascii")) - - -def gen_moves(board, captures_only=False, cap=256): - buf = (Move * cap)() - n = _lib.gen_pseudo_moves(C.byref(board), buf, captures_only) - return buf, n - - -def gen_legal_moves(board, out): - return int(get_legal_moves(C.byref(board), out)) - - -def is_square_attacked(board, sq, by): - return bool(square_attacked(C.byref(board), int(sq), int(by))) - - -def is_in_check(board, side): - return bool(in_check(C.byref(board), int(side))) - - -def get_attackers_to(board, sq, by): - return int(attackers_to(C.byref(board), int(sq), int(by))) - - -def sq_to_coord(sq): - return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8)) - - -def sq(name): - f = FILES[name[0].lower()] - r = int(name[1]) - 1 - return f + 8 * r - - -def bb_from(*algebraic): - m = 0 - for s in algebraic: - m |= (1 << sq(s)) - return m - - -def popcount(x: int) -> int: - return x.bit_count() - - -def draw_bb(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") \ No newline at end of file -- 2.34.1 From 8ef4a48ddca8050a9779b69a3333e203b6ea4e53 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Aug 2025 15:26:50 -0400 Subject: [PATCH 10/10] Remove uneeded c type references in signatures --- binding/python_c_ffi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/binding/python_c_ffi.py b/binding/python_c_ffi.py index 0f32325..5725959 100644 --- a/binding/python_c_ffi.py +++ b/binding/python_c_ffi.py @@ -127,20 +127,20 @@ class ChessFFI: 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) + n = self._c_gen_pseudo_moves(board, buf, captures_only) return buf, n def get_legal_moves(self, board, out): - return int(self._c_get_legal_moves(board, out)) + return self._c_get_legal_moves(board, out) def square_attacked(self, board, sq, by): - return bool(self._c_square_attacked(C.byref(board), int(sq), int(by))) + return self._c_square_attacked(board, sq, by) def in_check(self, board, side): - return bool(self._c_in_check(C.byref(board), int(side))) + return self._c_in_check(board, side) def attackers_to(self, board, sq, by): -- 2.34.1