diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index cf862db..c976bf3 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -575,6 +575,198 @@ bool square_attacked(const struct Board *board, int sq, enum Color by) { return attackers_to(board, sq, by) != 0; } +int piece_at(const struct Board *board, int sq) { + uint64_t mask = 1ULL << sq; + for (int piece = 0; piece < 12; ++piece) + if (board->pieces[piece] & mask) return piece; + return -1; +} + +void rebuild_occ(struct Board *board) { + uint64_t white=0; + uint64_t black=0; + + for (int p = P; p <= K; ++p) white |= board->pieces[p]; + for (int p = p; p <= k; ++p) black |= board->pieces[p]; + + board->occ[WHITE] = white; + board->occ[BLACK] = black; + board->occ[BOTH] = white | black; +} + +uint8_t castlerights_without(uint8_t castlerights, uint8_t mask) { + // Update castle rights by removing mask. + return castlerights & ~mask; +} + +uint8_t update_castlerights_after_move( + uint8_t cr, int moved_pid, int from, int to, int cap_pid +) { + // King moved: lose both + if (moved_pid == K) cr &= ~(CASTLE_WK | CASTLE_WQ); + if (moved_pid == k) cr &= ~(CASTLE_BK | CASTLE_BQ); + + // Rook moved from its home square + if (moved_pid == R) { + if (from == H1) cr &= ~CASTLE_WK; + if (from == A1) cr &= ~CASTLE_WQ; + } else if (moved_pid == r) { + if (from == H8) cr &= ~CASTLE_BK; + if (from == A8) cr &= ~CASTLE_BQ; + } + + // Captured a rook on its home square. + // Obviously if a rook has moved and is captured, + // the prior check accounts for that. + if (cap_pid == R) { + if (to == H1) cr &= ~CASTLE_WK; + if (to == A1) cr &= ~CASTLE_WQ; + } else if (cap_pid == r) { + if (to == H8) cr &= ~CASTLE_BK; + if (to == A8) cr &= ~CASTLE_BQ; + } + return cr; +} + +void put_piece(struct Board *board, int pid, int sq) { + board->pieces[pid] |= 1ULL << sq; +} + +void del_piece(struct Board *board, int pid, int sq) { + board->pieces[pid] &= ~(1ULL << sq); +} + +// Create a copy of the board to simulate move and test state. +bool apply_move_on_copy(struct Board *in, struct Board *out, struct Move m) { + *out = *in; + const enum Color us = in->side_to_move; + const enum Color opp = us ^ 1; + + // Reset enpassant. + out->ep_square = -1; + + // Handle capture / EP capture + int captured_pid = -1; + if (m.flags & MF_ENPASSANT) { + int cap_sq = (us == WHITE) ? (m.to - 8) : (m.to + 8); + captured_pid = piece_at(out, cap_sq); + if (captured_pid < 0) return false; + del_piece(out, captured_pid, cap_sq); + } else if (m.flags & MF_CAPTURE) { + captured_pid = piece_at(out, m.to); + if (captured_pid < 0) { + return false; + }; + del_piece(out, captured_pid, m.to); + } + + // Move (with promotion) + del_piece(out, m.piece, m.from); + // move.promo is the promotion piece. + int placed = (m.flags & MF_PROMO) ? m.promo : m.piece; + put_piece(out, placed, m.to); + + // Castling: move rook + if (m.flags & MF_CASTLE) { + if (us == WHITE) { + if (m.to == WK_TO) { + del_piece(out, R, H1); + put_piece(out, R, F1); + } + else { + del_piece(out, R, A1); + put_piece(out, R, D1); + } + } else { + if (m.to == BK_TO) { + del_piece(out, r, H8); + put_piece(out, r, F8); + } + else { + del_piece(out, r, A8); + put_piece(out, r, D8); + } + } + } + + // Double pawn push -> set EP square + if (m.flags & MF_DOUBLE_PUSH) out->ep_square = (us == WHITE) ? (m.to - 8) : (m.to + 8); + + // Update castling rights + out->castling_rights = update_castlerights_after_move( + in->castling_rights, m.piece, m.from, m.to, captured_pid + ); + + // Side to move flips + out->side_to_move = opp; + + rebuild_occ(out); + return true; +} + +bool castle_path_safe(const struct Board *board, enum Color side, int king_to) { + enum Color opp = side ^ 1; + if (side == WHITE) { + if (king_to == WK_TO) { + // E1-F1-G1 + if (square_attacked(board, E1, opp)) return false; + if (square_attacked(board, F1, opp)) return false; + } else { + // E1-D1-C1 + if (square_attacked(board, E1, opp)) return false; + if (square_attacked(board, D1, opp)) return false; + } + } else { + if (king_to == BK_TO) { + // E8-F8-G8 + if (square_attacked(board, E8, opp)) return false; + if (square_attacked(board, F8, opp)) return false; + } else { + // E8-D8-C8 + if (square_attacked(board, E8, opp)) return false; + if (square_attacked(board, D8, opp)) return false; + } + } + return true; +} + +bool move_is_legal(struct Board *board, struct Move move) { + enum Color us = board->side_to_move; + enum Color opp = us ^ 1; + + // Castle-through-check rule + if (move.flags & MF_CASTLE) { + if (!castle_path_safe(board, us, move.to)) { + return false; + } + } + + struct Board after; + if (!apply_move_on_copy(board, &after, move)) { + return false; + } + + + // Our king must not be in check after the move + uint64_t king_bb = (us == WHITE) ? after.pieces[K] : after.pieces[k]; + + if (!king_bb) return false; + + int king_sq = first_set_index(king_bb); + return !square_attacked(&after, king_sq, opp); +} + +int get_legal_moves(struct Board *board, struct Move *out) { + struct Move tmp[256]; + int n = gen_pseudo_moves(board, tmp, false); + + int count = 0; + for (int i = 0; i < n; ++i) + if (move_is_legal(board, tmp[i])) + out[count++] = tmp[i]; + return count; +} + void print_board(const struct Board *board) { const char PIECE_CH[12] = { 'P','N','B','R','Q','K', diff --git a/test/chess_ffi.py b/test/chess_ffi.py index 1c10502..7ad92ca 100644 --- a/test/chess_ffi.py +++ b/test/chess_ffi.py @@ -91,10 +91,12 @@ gen_king_moves = _bind_opt("gen_king_moves", *PIECE_SIG) 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) # Attack cache tables. @@ -126,6 +128,10 @@ def gen_moves(board, captures_only=False, cap=256): 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))) diff --git a/test/test_legal_move_gen.py b/test/test_legal_move_gen.py new file mode 100644 index 0000000..e85ea1b --- /dev/null +++ b/test/test_legal_move_gen.py @@ -0,0 +1,68 @@ +from test.base import ChessLibTestBase +from test.chess_ffi import Move +from test.chess_ffi import Board +from test.chess_ffi import sq_to_coord +from test.chess_ffi import gen_legal_moves + + +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) + return n, moves + + + def test_start_position(self): + # 20 legal moves from the initial chess position + fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + b = Board() + self.load_fen(fen, board=b) + + n, _ = self._gen_legal(b) + self.assertEqual(n, 20, "Start position must have 20 legal moves for White") + + + def test_kiwipete_depth1_count(self): + # Kiwipete: perft(1) = 48 + # position 2: https://www.chessprogramming.org/Perft_Results + fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" + b = Board(); self.load_fen(fen, board=b) + + n, moves = self._gen_legal(b) + self.assertEqual(n, 48, "Kiwipete perft(1) should be 48") + + + def test_perft_position_3(self): + fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1" + b = Board(); self.load_fen(fen, board=b) + + n, moves = self._gen_legal(b) + self.assertEqual(n, 14) + + + def test_perft_position_4(self): + fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" + b = Board(); self.load_fen(fen, board=b) + + n, moves = self._gen_legal(b) + self.assertEqual(n, 6) + + + def test_perft_position_5(self): + fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" + b = Board(); self.load_fen(fen, board=b) + + n, moves = self._gen_legal(b) + self.assertEqual(n, 44) + + + def test_pert_position_5(self): + fen = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" + b = Board(); self.load_fen(fen, board=b) + + n, moves = self._gen_legal(b) + self.assertEqual(n, 46) \ No newline at end of file