diff --git a/engine/include/bitboard.h b/engine/include/bitboard.h index c2ac730..acd9318 100644 --- a/engine/include/bitboard.h +++ b/engine/include/bitboard.h @@ -25,6 +25,39 @@ #define GET_BIT(bb, sq) ((bb) & (1ULL << (sq))) #define TOGGLE_BIT(bb, sq) ((bb) ^= (1ULL << (sq))) +// King castling helpers. +#define BB(sq) (1ULL << (sq)) + +// a1 = 0, h8 = 63 (rank*8 + file) +enum Square { + A1,B1,C1,D1,E1,F1,G1,H1, + A2,B2,C2,D2,E2,F2,G2,H2, + A3,B3,C3,D3,E3,F3,G3,H3, + A4,B4,C4,D4,E4,F4,G4,H4, + A5,B5,C5,D5,E5,F5,G5,H5, + A6,B6,C6,D6,E6,F6,G6,H6, + A7,B7,C7,D7,E7,F7,G7,H7, + A8,B8,C8,D8,E8,F8,G8,H8 +}; + +#define WK_EMPTY_MASK (BB(F1) | BB(G1)) +#define WQ_EMPTY_MASK (BB(B1) | BB(C1) | BB(D1)) +#define BK_EMPTY_MASK (BB(F8) | BB(G8)) +#define BQ_EMPTY_MASK (BB(B8) | BB(C8) | BB(D8)) + +// squares king is on / passes / lands (use for attacked-squares test later) +#define WK_THRU_MASK (BB(E1) | BB(F1) | BB(G1)) +#define WQ_THRU_MASK (BB(E1) | BB(D1) | BB(C1)) +#define BK_THRU_MASK (BB(E8) | BB(F8) | BB(G8)) +#define BQ_THRU_MASK (BB(E8) | BB(D8) | BB(C8)) + +// king target squares +#define WK_TO G1 +#define WQ_TO C1 +#define BK_TO G8 +#define BQ_TO C8 + + enum Color { WHITE = 0, BLACK = 1, BOTH = 2 }; enum Piece { P, N, B, R, Q, K, // 0..5 white @@ -38,6 +71,23 @@ enum Castling { CASTLE_BQ = 1 << 3 }; +enum MoveFlags { + MF_NONE = 0, + MF_CAPTURE = 1 << 0, + MF_PROMO = 1 << 1, + MF_ENPASSANT = 1 << 2, + MF_CASTLE = 1 << 3, + MF_DOUBLE_PUSH = 1 << 4, +}; + +struct Move { + uint16_t from; + uint16_t to; + uint8_t piece; + uint8_t promo; + uint8_t flags; +}; + struct Board { uint64_t pieces[12]; // Each set of pieces get a bitboard for each player. uint64_t occ[3]; // Color occupancy bitboards. @@ -55,4 +105,13 @@ struct Board { void create_knight_attack_cache(); void create_pawn_attack_cache(); void create_king_attack_cache(); -void print_board(); \ No newline at end of file +void print_board(); +void gen_white_pawn_quiet_pushes(const struct Board *board, struct Move *moves, int *count); +void gen_black_pawn_quiet_pushes(const struct Board *board, struct Move *moves, int *count); +void gen_white_pawn_push_promotions(const struct Board *board, struct Move *moves, int *count); +void gen_black_pawn_push_promotions(const struct Board *board, struct Move *moves, int *count); +void gen_white_pawn_captures(const struct Board *board, struct Move *moves, int *count); +void gen_black_pawn_captures(const struct Board *board, struct Move *moves, int *count); +void gen_white_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count); +void gen_black_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count); +int gen_pseudo_moves(const struct Board *board, struct Move *out, bool captures_only); \ No newline at end of file diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index f34239c..3583eb4 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -61,14 +61,14 @@ void create_king_attack_cache(void) { } } -static int first_set_index(uint64_t bb) { +int first_set_index(uint64_t bb) { for (int i = 0; i < 64; ++i) { if ((bb >> i) & 1ULL) return i; } return -1; } -static int pop_lsb_index(uint64_t *bb) { +int pop_lsb_index(uint64_t *bb) { if (*bb == 0) return -1; int idx = first_set_index(*bb); // Clears bit. @@ -76,8 +76,463 @@ static int pop_lsb_index(uint64_t *bb) { return idx; } -void print_board(const struct Board *b) { - static const char PIECE_CH[12] = { +uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, uint64_t occupied) +{ + uint64_t attacks = 0; + + int start_file = start_square % 8; + int start_rank = start_square / 8; + + int next_file = start_file + delta_file; + int next_rank = start_rank + delta_rank; + + while (next_file >= 0 && next_file < 8 && next_rank >= 0 && next_rank < 8) { + int next_square = next_rank * 8 + next_file; + uint64_t next_bit = 1ULL << next_square; + + // can attack this square + attacks |= next_bit; + + // blocker: include it, then stop + if (occupied & next_bit) + break; + + next_file += delta_file; + next_rank += delta_rank; + } + + return attacks; +} + +uint64_t rook_attacks(int square, uint64_t occupied) { + return ray_attacks(square, +1, 0, occupied) // east + | ray_attacks(square, -1, 0, occupied) // west + | ray_attacks(square, 0, +1, occupied) // north + | ray_attacks(square, 0, -1, occupied); // south +} + +uint64_t bishop_attacks(int square, uint64_t occupied) { + return ray_attacks(square, +1, +1, occupied) // NE + | ray_attacks(square, -1, +1, occupied) // NW + | ray_attacks(square, +1, -1, occupied) // SE + | ray_attacks(square, -1, -1, occupied); // SW +} + +uint64_t queen_attacks(int square, uint64_t occupied) { + // Simply combine both types of attacks + return rook_attacks(square, occupied) | bishop_attacks(square, occupied); +} + +void push_move(struct Move *out, int *count, int from, int to, uint8_t piece, uint8_t promo, uint8_t flags) +{ + out[*count] = (struct Move){ + .from = (uint16_t)from, + .to = (uint16_t)to, + .piece = piece, + .promo = promo, // 0 for non-promo; + .flags = flags + }; + (*count)++; +} + +/** + PAWN struct Movement Psuedo Moves + + + Separate the white and black pawn logic. Although the logic is similar, + by separating the logic, we can isolate the different cases and have clearer parent functions. + These pieces are also the only ones that move in opposite ways, meaning we only need + to separate out seemingly related code for this case. Additionally, pawns have the + most complicated movement patterns and behaviors in the game. Separating out the logic should + hopefully make debugging easier. +*/ +void gen_white_pawn_quiet_pushes(const struct Board *board, struct Move *out, int *count) { + const uint64_t occ = board->occ[BOTH]; + const uint64_t empty = ~occ; + + const uint64_t pawns = board->pieces[P]; + + // One-step pushes: shift pawns up by 8 onto empty squares + uint64_t one_step = (pawns << 8) & empty; + + // Exclude promotions here (dest on rank 8) — handled by a separate promo-push function + uint64_t one_step_no_promo = one_step & ~RANK_8; + + // Two-step pushes: those one-step pawns that landed on rank 3 can go one more if still empty + // (means they originally stood on rank 2) + uint64_t two_step = ((one_step & RANK_3) << 8) & empty; + + uint64_t bb = one_step_no_promo; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to - 8; + push_move(out, count, from, to, P, 0, MF_NONE); + } + + bb = two_step; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to - 16; + push_move(out, count, from, to, P, 0, MF_DOUBLE_PUSH); + } +} + +void gen_black_pawn_quiet_pushes(const struct Board *board, struct Move *out, int *count) { + const uint64_t occ = board->occ[BOTH]; + const uint64_t empty = ~occ; + + const uint64_t pawns = board->pieces[p]; + + // One-step pushes: shift pawns down by 8 onto empty squares + uint64_t one_step = (pawns >> 8) & empty; + + // Exclude promotions here (dest on rank 1) — handle in a promo-push function + uint64_t one_step_no_promo = one_step & ~RANK_1; + + uint64_t two_step = ((one_step & RANK_6) >> 8) & empty; + + uint64_t bb = one_step_no_promo; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to + 8; + push_move(out, count,from, to, p, 0, MF_NONE); + } + + bb = two_step; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to + 16; + push_move(out, count,from, to, p, 0, MF_DOUBLE_PUSH); + } +} + +// We will only allow pawns to promote to queen. Technically, they should be allowed +// to promote to any of the following: Rook, Bishop, Knight, Queen. +void gen_white_pawn_push_promotions(const struct Board *board, struct Move *out, int *count) { + const uint64_t occ = board->occ[BOTH]; + const uint64_t empty = ~occ; + const uint64_t pawns = board->pieces[P]; + + // destinations on rank 8 reachable by a single push + uint64_t promos = ((pawns << 8) & empty) & RANK_8; + + while (promos) { + int to = pop_lsb_index(&promos); + int from = to - 8; + push_move(out, count, from, to, P, Q, MF_PROMO); + } +} + +void gen_black_pawn_push_promotions(const struct Board *board, struct Move *out, int *count) { + const uint64_t occ = board->occ[BOTH]; + const uint64_t empty = ~occ; + const uint64_t pawns = board->pieces[p]; + + // destinations on rank 1 reachable by a single push + uint64_t promos = ((pawns >> 8) & empty) & RANK_1; + + while (promos) { + int to = pop_lsb_index(&promos); + int from = to + 8; + push_move(out, count, from, to, p, q, MF_PROMO); + } +} + +void gen_white_pawn_capture_promotions(const struct Board *board, struct Move *out, int *count) { + const uint64_t opp = board->occ[BLACK]; + // left capture (from white view): +7, mask off file A + uint64_t left = ((board->pieces[P] & ~FILE_A) << 7) & opp & RANK_8; + while (left) { + int to = pop_lsb_index(&left); + int from = to - 7; + push_move(out, count,from, to, P, Q, MF_CAPTURE | MF_PROMO); + } + // right capture: +9, mask off file H + uint64_t right = ((board->pieces[P] & ~FILE_H) << 9) & opp & RANK_8; + while (right) { + int to = pop_lsb_index(&right); + int from = to - 9; + push_move(out, count,from, to, P, Q, MF_CAPTURE | MF_PROMO); + } +} + +void gen_black_pawn_capture_promotions(const struct Board *board, struct Move *out, int *count) { + const uint64_t opp = board->occ[WHITE]; + // from black view, “left” is -7 (mask off file H before shifting) + uint64_t left = ((board->pieces[p] & ~FILE_H) >> 7) & opp & RANK_1; + while (left) { + int to = pop_lsb_index(&left); + int from = to + 7; + push_move(out, count,from, to, p, q, MF_CAPTURE | MF_PROMO); + } + // “right” is -9 (mask off file A) + uint64_t right = ((board->pieces[p] & ~FILE_A) >> 9) & opp & RANK_1; + while (right) { + int to = pop_lsb_index(&right); + int from = to + 9; + push_move(out, count,from, to, p, q, MF_CAPTURE | MF_PROMO); + } +} + +void gen_white_pawn_captures(const struct Board *board, struct Move *out, int *count) { + const uint64_t pawns = board->pieces[P]; + const uint64_t opp = board->occ[BLACK]; + + // Normal captures (exclude promotion rank) + uint64_t left_caps = ((pawns & ~FILE_A) << 7) & opp & ~RANK_8; + uint64_t right_caps = ((pawns & ~FILE_H) << 9) & opp & ~RANK_8; + + // Emit normal captures + while (left_caps) { + int to = pop_lsb_index(&left_caps); + int from = to - 7; + push_move(out, count,from, to, P, 0, MF_CAPTURE); + } + while (right_caps) { + int to = pop_lsb_index(&right_caps); + int from = to - 9; + push_move(out, count,from, to, P, 0, MF_CAPTURE); + } + + // En passant (destination is ep_square) + if (board->ep_square >= 0) { + uint64_t ep = 1ULL << board->ep_square; + + // From-squares that can capture onto ep (reverse of +7/+9) + uint64_t ep_from = (((ep & ~FILE_H) >> 7) | ((ep & ~FILE_A) >> 9)) & pawns; + + while (ep_from) { + int from = pop_lsb_index(&ep_from); + int to = board->ep_square; + // EP never promotes, so no promo piece; still a capture + push_move(out, count,from, to, P, 0, MF_CAPTURE | MF_ENPASSANT); + } + } +} + +void gen_black_pawn_captures(const struct Board *board, struct Move *out, int *count) { + const uint64_t pawns = board->pieces[p]; + const uint64_t opp = board->occ[WHITE]; + + // Normal captures (exclude promotion rank) + uint64_t left_caps = ((pawns & ~FILE_H) >> 7) & opp & ~RANK_1; // from black POV, "left" is -7 + uint64_t right_caps = ((pawns & ~FILE_A) >> 9) & opp & ~RANK_1; // "right" is -9 + + while (left_caps) { + int to = pop_lsb_index(&left_caps); + int from = to + 7; + push_move(out, count,from, to, p, 0, MF_CAPTURE); + } + while (right_caps) { + int to = pop_lsb_index(&right_caps); + int from = to + 9; + push_move(out, count,from, to, p, 0, MF_CAPTURE); + } + + // En passant + if (board->ep_square >= 0) { + uint64_t ep = 1ULL << board->ep_square; + + // From-squares that can capture onto ep (reverse of -7/-9) + uint64_t ep_from = (((ep & ~FILE_A) << 7) | ((ep & ~FILE_H) << 9)) & pawns; + + while (ep_from) { + int from = pop_lsb_index(&ep_from); + int to = board->ep_square; + push_move(out, count,from, to, p, 0, MF_CAPTURE | MF_ENPASSANT); + } + } +} + +/** + + + Pieces that are not PAWNs + + +*/ +void gen_knight_moves(const struct Board *board, struct Move *out, int *count, bool captures_only) { + enum Color side = board->side_to_move; + uint64_t own = board->occ[side]; + uint64_t opp = board->occ[side ^ 1]; + uint8_t pid = (side == WHITE) ? N : n; + uint64_t bb = (side == WHITE) ? board->pieces[N] : board->pieces[n]; + + while (bb) { + int from = pop_lsb_index(&bb); + uint64_t mask = KNIGHT_ATTACKS[from] & ~own; + uint64_t caps = mask & opp; + + if (!captures_only) { + uint64_t quiet = mask & ~opp; + while (quiet) { + int to = pop_lsb_index(&quiet); + push_move(out, count, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, count, from, to, pid, 0, MF_CAPTURE); + } + } +} + +void gen_bishop_moves(const struct Board *board, struct Move *out, int *count, bool captures_only) { + enum Color side = board->side_to_move; + uint64_t own = board->occ[side]; + uint64_t opp = board->occ[side ^ 1]; + uint64_t occ = board->occ[BOTH]; + uint8_t pid = (side == WHITE) ? B : b; + uint64_t bb = (side == WHITE) ? board->pieces[B] : board->pieces[b]; + + while (bb) { + int from = pop_lsb_index(&bb); + uint64_t mask = bishop_attacks(from, occ) & ~own; + uint64_t caps = mask & opp; + + if (!captures_only) { + uint64_t quiet = mask & ~opp; + while (quiet) { + int to = pop_lsb_index(&quiet); + push_move(out, count, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, count, from, to, pid, 0, MF_CAPTURE); + } + } +} + +void gen_rook_moves(const struct Board *board, struct Move *out, int *count, bool captures_only) { + enum Color side = board->side_to_move; + uint64_t own = board->occ[side]; + uint64_t opp = board->occ[side ^ 1]; + uint64_t occ = board->occ[BOTH]; + uint8_t pid = (side == WHITE) ? R : r; + uint64_t bb = (side == WHITE) ? board->pieces[R] : board->pieces[r]; + + while (bb) { + int from = pop_lsb_index(&bb); + uint64_t mask = rook_attacks(from, occ) & ~own; + uint64_t caps = mask & opp; + + if (!captures_only) { + uint64_t quiet = mask & ~opp; + while (quiet) { + int to = pop_lsb_index(&quiet); + push_move(out, count,from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); + } + } +} + +void gen_queen_moves(const struct Board *board, struct Move *out, int *count, bool captures_only) { + enum Color side = board->side_to_move; + uint64_t own = board->occ[side]; + uint64_t opp = board->occ[side ^ 1]; + uint64_t occ = board->occ[BOTH]; + uint8_t pid = (side == WHITE) ? Q : q; + uint64_t bb = (side == WHITE) ? board->pieces[Q] : board->pieces[q]; + + while (bb) { + int from = pop_lsb_index(&bb); + uint64_t mask = (rook_attacks(from, occ) | bishop_attacks(from, occ)) & ~own; + uint64_t caps = mask & opp; + + if (!captures_only) { + uint64_t quiet = mask & ~opp; + while (quiet) { + int to = pop_lsb_index(&quiet); + push_move(out, count,from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); + } + } +} + +void gen_king_moves(const struct Board *board, struct Move *out, int *count, bool captures_only) { + enum Color side = board->side_to_move; + uint64_t own = board->occ[side]; + uint64_t opp = board->occ[side ^ 1]; + uint8_t pid = (side == WHITE) ? K : k; + uint64_t kk = (side == WHITE) ? board->pieces[K] : board->pieces[k]; + + if (!kk) return; + int from = first_set_index(kk); + uint64_t mask = KING_ATTACKS[from] & ~own; + uint64_t caps = mask & opp; + + if (!captures_only) { + uint64_t quiet = mask & ~opp; + while (quiet) { + int to = pop_lsb_index(&quiet); + push_move(out, count,from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); + } + + if (!captures_only) { + uint64_t occ = board->occ[BOTH]; + if (side == WHITE) { + if ((board->castling_rights & CASTLE_WK) && !(occ & WK_EMPTY_MASK)) { + push_move(out, count,E1, WK_TO, K, 0, MF_CASTLE); + } + if ((board->castling_rights & CASTLE_WQ) && !(occ & WQ_EMPTY_MASK)) { + push_move(out, count,E1, WQ_TO, K, 0, MF_CASTLE); + } + } else { + if ((board->castling_rights & CASTLE_BK) && !(occ & BK_EMPTY_MASK)) { + push_move(out, count,E8, BK_TO, k, 0, MF_CASTLE); + } + if ((board->castling_rights & CASTLE_BQ) && !(occ & BQ_EMPTY_MASK)) { + push_move(out, count,E8, BQ_TO, k, 0, MF_CASTLE); + } + } + } +} + +int gen_pseudo_moves(const struct Board *board, struct Move *moves, bool captures_only) { + int count = 0; + + if (board->side_to_move == WHITE) { + if (!captures_only) { + gen_white_pawn_quiet_pushes(board, moves, &count); + gen_white_pawn_push_promotions(board, moves, &count); + } + gen_white_pawn_captures(board, moves, &count); + gen_white_pawn_capture_promotions(board, moves, &count); + } else { + if (!captures_only) { + gen_black_pawn_quiet_pushes(board, moves, &count); + gen_black_pawn_push_promotions(board, moves, &count); + } + gen_black_pawn_captures(board, moves, &count); + gen_black_pawn_capture_promotions(board, moves, &count); + } + + gen_knight_moves(board, moves, &count, captures_only); + gen_king_moves(board, moves, &count, captures_only); + gen_rook_moves(board, moves, &count, captures_only); + gen_bishop_moves(board, moves, &count, captures_only); + gen_queen_moves(board, moves, &count, captures_only); + + return count; +} + + +void print_board(const struct Board *board) { + const char PIECE_CH[12] = { 'P','N','B','R','Q','K', 'p','n','b','r','q','k' }; @@ -86,7 +541,7 @@ void print_board(const struct Board *b) { for (int i = 0; i < 64; ++i) grid[i] = '.'; for (int p = 0; p < 12; ++p) { - uint64_t bb = b->pieces[p]; + uint64_t bb = board->pieces[p]; while (bb) { int sq = pop_lsb_index(&bb); grid[sq] = PIECE_CH[p]; diff --git a/engine/src/test.c b/engine/src/test.c index 47e5514..2ab2bf7 100644 --- a/engine/src/test.c +++ b/engine/src/test.c @@ -1,33 +1,116 @@ -/** - A quick test script to debug and visually inspect functions - and operations. -*/ - - +// engine/src/test_pawns.c #include #include #include "bitboard.h" #include "fen.h" +enum { MAX_MOVES = 256 }; -static int popcount64(uint64_t x){ int c=0; while(x){ x&=x-1; ++c; } return c; } -static const char NAME[12] = {'P','N','B','R','Q','K','p','n','b','r','q','k'}; +void sq_to_coord(int sq, char out[3]) { + out[0] = 'a' + (sq % 8); + out[1] = '1' + (sq / 8); + out[2] = '\0'; +} +void print_move(struct Move *m) { + char f[3], t[3]; + sq_to_coord(m->from, f); + sq_to_coord(m->to, t); + + printf("%s%s", f, t); + if (m->flags) { + printf(" ["); + int first = 1; + if (m->flags & MF_CAPTURE) { printf("%sCAP", first?"":"|"); first = 0; } + if (m->flags & MF_DOUBLE_PUSH) { printf("%sDP", first?"":"|"); first = 0; } + if (m->flags & MF_ENPASSANT) { printf("%sEP", first?"":"|"); first = 0; } + if (m->flags & MF_PROMO) { printf("%sPROMO->%c", first?"":"|", (char)(m->promo ? "PNBRQKpnbrqk"[m->promo] : 'Q')); } + printf("]"); + } +} + +void dump_moves(const char *title, struct Move *moves, int count) { + printf("%s (%d):\n", title, count); + for (int i = 0; i < count; ++i) { print_move(&moves[i]); putchar('\n'); } + putchar('\n'); +} int main(void) { - const char *fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + // change this FEN to target specific cases you want to test + //const char *fen_startpos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + const char *fen_startpos = "8/8/8/8/8/8/8/8 w KQkq - 0 1"; + const char *fen = fen_startpos; struct Board b; + memset(&b, 0, sizeof(b)); - int rc = load_fen(&b, fen); - - for (int i=0;i<12;i++) printf("%c: %d\n", NAME[i], popcount64(b.pieces[i])); - - if (rc != 0) { - fprintf(stderr, "load_fen failed (rc=%d) for FEN:\n%s\n", rc, fen); + if (load_fen(&b, fen) != 0) { + fprintf(stderr, "load_fen failed\n"); return 1; } - print_board(&b); + struct Move moves[MAX_MOVES]; + int count; + + /* + // --- WHITE pawns: quiet pushes (single + double, no promos) --- + count = 0; + gen_white_pawn_quiet_pushes(&b, moves, &count); + dump_moves("white quiet pushes", moves, count); + + + // --- BLACK pawns: quiet pushes --- + gen_black_pawn_quiet_pushes(&b, moves, &count); + dump_moves("black quiet pushes", moves, count); + + printf("COUNT: %d", count); + */ + + create_knight_attack_cache(); + //create_king_attack_cache(); + //create_pawn_attack_cache(); + + int n = gen_pseudo_moves(&b, moves, false); + printf("COUNT: %d", n); + + /* + // --- WHITE pawns: quiet push promotions (queen-only) --- + count = 0; + gen_white_pawn_push_promotions_q(&b, moves, &count); + dump_moves("white push promotions (Q only)", moves, count); + + // --- WHITE pawns: captures (non-promo) + en passant --- + count = 0; + gen_white_pawn_captures(&b, moves, &count); + dump_moves("white captures (non-promo + EP)", moves, count); + + // --- WHITE pawns: capture promotions (queen-only) --- + count = 0; + gen_white_pawn_capture_promotions_q(&b, moves, &count); + dump_moves("white capture promotions (Q only)", moves, count); + + // Flip side to move if you want to test black without changing FEN side + b.side_to_move = BLACK; + + // --- BLACK pawns: quiet pushes --- + count = 0; + gen_black_pawn_quiet_pushes(&b, moves, &count); + dump_moves("black quiet pushes", moves, count); + + // --- BLACK pawns: quiet push promotions (queen-only) --- + count = 0; + gen_black_pawn_push_promotions_q(&b, moves, &count); + dump_moves("black push promotions (q only)", moves, count); + + // --- BLACK pawns: captures (non-promo) + en passant --- + count = 0; + gen_black_pawn_captures(&b, moves, &count); + dump_moves("black captures (non-promo + EP)", moves, count); + + // --- BLACK pawns: capture promotions (queen-only) --- + count = 0; + gen_black_pawn_capture_promotions_q(&b, moves, &count); + dump_moves("black capture promotions (q only)", moves, count); + */ return 0; -} \ No newline at end of file +} diff --git a/makefile b/makefile index 6169d94..4c0d37e 100644 --- a/makefile +++ b/makefile @@ -17,7 +17,7 @@ TESTSRC := $(SRCDIR)/test.c TESTOBJ := $(BUILDDIR)/test.o TESTBIN := $(BUILDDIR)/print_board -.PHONY: all clean test test-exe run +.PHONY: all clean test test-exe run-c-test all: $(LIB) @@ -27,18 +27,19 @@ $(BUILDDIR): $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR) $(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@ -$(LIB): $(OBJ) +$(LIB): $(OBJ) | $(BUILDDIR) $(CC) $(LDFLAGS) -o $@ $^ -c-test-exe: $(TESTBIN) +# ---- test exe rules ---- +test-exe: $(TESTBIN) $(TESTOBJ): $(TESTSRC) | $(BUILDDIR) - $(CC) -std=c11 -Wall -Wextra -O2 -I$(INCDIR) -c $< -o $@ + $(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@ -$(TESTBIN): $(TESTOBJ) $(LIB) +$(TESTBIN): $(TESTOBJ) $(LIB) | $(BUILDDIR) $(CC) -O2 -o $@ $(TESTOBJ) -L$(BUILDDIR) -lchess -Wl,-rpath,'$$ORIGIN' -run-c-test: test-exe +run-c-test: $(TESTBIN) LD_LIBRARY_PATH=$(BUILDDIR) $(TESTBIN) $(FEN) clean: diff --git a/test/base.py b/test/base.py index 5162eca..02b4d63 100644 --- a/test/base.py +++ b/test/base.py @@ -1,79 +1,33 @@ -import ctypes -import platform import unittest -from pathlib import Path - - -WHITE, BLACK = 0, 1 -FILES = {c:i for i,c in enumerate("abcdefgh")} - - -def sq(name: str) -> int: - 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: int, origin: int | None = None) -> str: - 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") +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): - root = Path(__file__).resolve().parents[1] - libname = "libchess.so" - libpath = root / "build" / libname - cls.lib = ctypes.CDLL(str(libpath)) + init_attack_caches() - attack_cache_functions = [ - "create_knight_attack_cache", - "create_pawn_attack_cache", - "create_king_attack_cache", - ] + cls.KNIGHT_ATTACKS = KNIGHT_ATTACKS + cls.KING_ATTACKS = KING_ATTACKS + cls.PAWN_ATTACKS = PAWN_ATTACKS - # init functions - for fn in attack_cache_functions: - getattr(cls.lib, fn).argtypes = [] - getattr(cls.lib, fn).restype = None - - cls.lib.create_knight_attack_cache() - cls.lib.create_pawn_attack_cache() - cls.lib.create_king_attack_cache() - KnightArr = ctypes.c_uint64 * 64 - KingArr = ctypes.c_uint64 * 64 - PawnRow = ctypes.c_uint64 * 64 - PawnArr = PawnRow * 2 + def setUp(self): + # This should be an empty board for each test in the suite. + self.board = Board() - cls.KNIGHT_ATTACKS = KnightArr.in_dll(cls.lib, "KNIGHT_ATTACKS") - cls.KING_ATTACKS = KingArr.in_dll(cls.lib, "KING_ATTACKS") - cls.PAWN_ATTACKS = PawnArr.in_dll(cls.lib, "PAWN_ATTACKS") \ No newline at end of file + + def load_fen(self, fen, board=None): + if board: + return load_fen(board, fen) + return 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 diff --git a/test/chess_ffi.py b/test/chess_ffi.py new file mode 100644 index 0000000..b0abc45 --- /dev/null +++ b/test/chess_ffi.py @@ -0,0 +1,158 @@ +""" + 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 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 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: int, origin: int | None = None) -> str: + 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 diff --git a/test/test_fen_loader.py b/test/test_fen_loader.py index 088b0ff..37620b0 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -1,81 +1,65 @@ -import ctypes from test.base import ChessLibTestBase -from test.base import BLACK -from test.base import WHITE -from test.base import sq -from test.base import popcount +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 -class Board(ctypes.Structure): - _fields_ = [ - ("pieces", ctypes.c_uint64 * 12), - ("occ", ctypes.c_uint64 * 3), - ("king_square", ctypes.c_uint64 * 2), - ("castling_rights", ctypes.c_uint8), - ("ep_square", ctypes.c_int), - ("side_to_move", ctypes.c_int), - ("halfmove_clock", ctypes.c_int), - ("fullmove_number", ctypes.c_int), - ] -class FenTests(ChessLibTestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.lib.load_fen.argtypes = [ctypes.POINTER(Board), ctypes.c_char_p] - cls.lib.load_fen.restype = ctypes.c_int +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): fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" - b = Board() + rc = self.load_fen(fen) - self.lib.load_fen(ctypes.byref(b), fen.encode("ascii")) + 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) # KQkq - 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) - white_expected = self.rank_mask(0) | self.rank_mask(1) # ranks 1 & 2 - black_expected = self.rank_mask(6) | self.rank_mask(7) # ranks 7 & 8 - 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) + self.assertEqual(popcount(int(self.board.occ[WHITE])), 16) + self.assertEqual(popcount(int(self.board.occ[BLACK])), 16) def test_castling_flags_parsing(self): fen = "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 5 42" - b = Board() - self.lib.load_fen(ctypes.byref(b), fen.encode("ascii")) - 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) + + 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) def test_en_passant_targets(self): # EP at e6, black to move - fen1 = "8/8/8/3pP3/8/8/8/8 b KQkq e6 0 1" - - b1 = Board() - self.lib.load_fen(ctypes.byref(b1), fen1.encode("ascii")) - self.assertEqual(b1.side_to_move, BLACK) - self.assertEqual(b1.ep_square, sq("e6")) + fen = "8/8/8/3pP3/8/8/8/8 b KQkq e6 0 1" + rc = self.load_fen(fen) + self.assertEqual(rc, 0) + self.assertEqual(self.board.side_to_move, BLACK) + self.assertEqual(self.board.ep_square, sq("e6")) - # EP at d3, white to move - fen2 = "8/8/8/8/3Pp3/8/8/8 w KQkq d3 12 7" - b2 = Board() - self.lib.load_fen(ctypes.byref(b2), fen2.encode("ascii")) - self.assertEqual(b2.side_to_move, WHITE) - self.assertEqual(b2.ep_square, sq("d3")) + # EP at d3, white to move — use a fresh Board + fen = "8/8/8/8/3Pp3/8/8/8 w KQkq d3 12 7" + b = Board() + rc = self.load_fen(fen, board=b) + self.assertEqual(rc, 0) + self.assertEqual(b.side_to_move, WHITE) + self.assertEqual(b.ep_square, sq("d3")) def test_malformed_piece_field(self): @@ -86,8 +70,7 @@ class FenTests(ChessLibTestBase): "8/8/8/8/8/8/8/8w - - 0 1", ] for fen in bad: - b = Board() - r = self.lib.load_fen(ctypes.byref(b), fen.encode("ascii")) + r = self.load_fen(fen, board=Board()) self.assertEqual(r, -1) @@ -99,6 +82,5 @@ class FenTests(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: - b = Board() - r = self.lib.load_fen(ctypes.byref(b), fen.encode("ascii")) + r = self.load_fen(fen, board=Board()) self.assertEqual(r, -1) \ No newline at end of file diff --git a/test/test_movegen.py b/test/test_movegen.py new file mode 100644 index 0000000..c809061 --- /dev/null +++ b/test/test_movegen.py @@ -0,0 +1,286 @@ +import ctypes +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 + + +class TestPseudoMoveGeneration(ChessLibTestBase): + def run_subtests(self, cases, move_function, captures_only=None): + for fen, expected, msg in cases: + with self.subTest(msg=msg, fen=fen): + cnt = ctypes.c_int(0) + moves = (Move * MAX_MOVES)() + b = Board() + + self.load_fen(fen, board=b) + if captures_only is not None: + move_function(b, moves, ctypes.byref(cnt), captures_only) + else: + # Account for pawn move gen. + move_function(b, moves, ctypes.byref(cnt)) + self.assertEqual(expected, cnt.value) + + + def test_knight_move_gen(self): + all_move_types = [ + ("8/8/8/8/3N4/8/8/8 w - - 0 1", 8, "center d4: 8 moves"), + ("8/8/8/8/N7/8/8/8 w - - 0 1", 4, "edge a4: 4 moves"), + ("8/8/8/8/8/8/8/N7 w - - 0 1", 2, "corner a1: 2 moves"), + ("8/8/8/8/8/8/8/1N6 w - - 0 1", 3, "b1: 3 moves"), + + # own pieces on destinations reduce count + ("8/8/8/5P2/3N4/1P6/2P5/8 w - - 0 1", 5, "d4 with own pawns on f5,b3,c2 -> 8-3=5"), + + # two knights + ("8/8/8/8/8/8/8/1N4N1 w - - 0 1", 6, "b1 & g1, empty board: 3+3"), + + # all 8 destinations occupied by black (still 8 total moves, all as captures) + ("8/8/2p1p3/1p3p2/3N4/1p3p2/2p1p3/8 w - - 0 1", 8, "enemy on all 8 targets"), + + # corner with one capture and one friendly block + ("8/8/8/8/8/1p6/2P5/N7 w - - 0 1", 1, "a1, b3 black (capture), c2 white (blocked)"), + ] + + captures_only = [ + ("8/8/8/8/3N4/8/8/8 w - - 0 1", 0, "center d4 but no enemies"), + ("8/8/2p5/1p6/3N4/5p2/8/8 w - - 0 1", 3, "d4 capturing c6,b5,f3"), + ("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) + + + def test_bishop_move_gen(self): + cases_bishop_all = [ + ("8/8/8/8/3B4/8/8/8 w - - 0 1", 13, "center d4: 13 moves"), + ("8/8/8/8/B7/8/8/8 w - - 0 1", 7, "edge a4: 7 moves"), + ("8/8/8/8/8/8/8/B7 w - - 0 1", 7, "corner a1: 7 moves"), + ("8/8/8/8/8/8/8/1B6 w - - 0 1", 7, "b1: 7 moves"), + + # all four diagonals blocked immediately by own pieces -> 0 + ("8/8/8/2P1P3/3B4/2P1P3/8/8 w - - 0 1", 0, "d4 with white pawns on c3,e3,c5,e5"), + + # all four diagonals have a black piece on the first square -> 4 (all captures) + ("8/8/8/2p1p3/3B4/2p1p3/8/8 w - - 0 1", 4, "d4 with black pawns on c3,e3,c5,e5"), + + # mixed blockers: NE own on f6, SW capture on b2, SE capture on e3, NW open + ("8/8/1p3P2/8/3B4/4p3/1p6/8 w - - 0 1", 6, ""), + ] + + cases_bishop_caps = [ + ("8/8/8/8/3B4/8/8/8 w - - 0 1", 0, "no enemies to capture"), + + # first squares on all rays are enemies → 4 captures + ("8/8/8/2p1p3/3B4/2p1p3/8/8 w - - 0 1", 4, "adjacent caps on c3,e3,c5,e5"), + + # enemies at far ends on each ray → still 4 captures (one per ray) + ("7p/p7/8/8/3B4/8/8/p5p1 w - - 0 1", 4, "targets h8,a7,g1,a1"), + + # 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) + + + def test_rook_move_gen(self): + cases_rook_all = [ + ("8/8/8/8/3R4/8/8/8 w - - 0 1", 14, "center d4: 14 moves on empty board"), + ("8/8/8/8/R7/8/8/8 w - - 0 1", 14, "edge a4: still 14 on empty board"), + ("8/8/8/8/8/8/8/R7 w - - 0 1", 14, "corner a1: 14 on empty board"), + + # all four adjacents are own pieces → no squares available + ("8/8/8/3P4/2PRP3/3P4/8/8 w - - 0 1", 0, "d4 with white pawns on c4,d5,e4,d3 (fully blocked)"), + + # all four adjacents are enemy pieces → exactly 4 captures + ("8/8/8/3p4/2pRp3/3p4/8/8 w - - 0 1", 4, "d4 with black pawns on c4,d5,e4,d3 (adjacent captures)"), + + # mixed: horizontal blocked by own, vertical has one capture each way + ("8/8/3p4/8/2PRP3/8/3p4/8 w - - 0 1", 4, "d4; own c4/e4 block left/right; captures at d5,d3"), + + # two rooks on a1 and h1 block each other along rank 1 + ("8/8/8/8/8/8/8/R6R w - - 0 1", 26, "a1 and h1: 13 each (not 14) because they block each other on rank"), + ] + + cases_rook_caps = [ + ("8/8/8/8/3R4/8/8/8 w - - 0 1", 0, + "no enemies at all"), + + # adjacent enemies in all 4 directions → 4 captures + ("8/8/8/3p4/2pRp3/3p4/8/8 w - - 0 1", 4, + "adjacent captures at c4,d5,e4,d3"), + + # two far enemies on clear rays → 2 captures (h4 and d8) + ("3r4/8/8/8/3R3r/8/8/8 w - - 0 1", 2, + "captures on d8 and h4"), + + # mixed: own pieces block two rays; captures on the other two (a4 and d1) + ("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) + + + def test_queen_move_gen(self): + cases_queen_all = [ + ("8/8/8/8/3Q4/8/8/8 w - - 0 1", 27, "center d4: rook(14)+bishop(13)"), + + ("8/8/8/8/Q7/8/8/8 w - - 0 1", 21, "edge a4: 14+7"), + ("8/8/8/8/8/8/8/Q7 w - - 0 1", 21, "corner a1: 14+7"), + ("8/8/8/8/8/8/8/1Q6 w - - 0 1", 21, "b1: 14+7"), + + # all 8 adjacent squares are friendly ⇒ blocked immediately on all rays + ("8/8/8/2PPP3/2PQP3/2PPP3/8/8 w - - 0 1", 0, "d4 with white pawns on c3,d3,e3,c4,e4,c5,d5,e5"), + + # all 8 adjacent squares are enemies ⇒ exactly 8 captures (one per ray), no further squares + ("8/8/8/2p1p3/2pQp3/2p1p3/8/8 w - - 0 1", 13, "d4 with black pawns adjacent on all rays"), + + # only horizontal blocked by own pieces at c4/e4; vertical+diagonals open + ("8/8/8/8/2PQP3/8/8/8 w - - 0 1", 20, "d4, own at c4,e4: rook loses 7→7 (up+down only), bishop 13 ⇒ 20"), + + # two queens on a1 & h1 block each other along rank 1 + ("8/8/8/8/8/8/8/Q6Q w - - 0 1", 40, "a1 & h1: each 21→20 due to mutual block ⇒ 40 total"), + ] + + cases_queen_caps = [ + ("8/8/8/8/3Q4/8/8/8 w - - 0 1", 0, "no enemies to capture"), + + # adjacent enemies only → 6 captures + ("8/8/8/2p1p3/2pQp3/2p1p3/8/8 w - - 0 1", 6, "adjacent caps on all rays"), + + # adjacent enemies only → 8 captures + ("8/8/8/2ppp3/2pQp3/2ppp3/8/8 w - - 0 1", 8, "adjacent caps on all rays"), + + # far targets on two rook rays (d8 and h4) + friendly blocking b1 + ("3r4/8/8/8/1P1Q3r/8/8/8 w - - 0 1", 2, "captures on d8 and h4; b4 friend blocks left; no other enemies"), + + # 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) + + + def test_king_move_gen(self): + cases_king_all = [ + ("8/8/8/8/3K4/8/8/8 w - - 0 1", 8, "center d4: 8 moves"), + ("8/8/8/8/K7/8/8/8 w - - 0 1", 5, "edge a4: 5 moves"), + ("8/8/8/8/8/8/8/K7 w - - 0 1", 3, "corner a1: 3 moves"), + + # all eight adjacent squares are WHITE pieces → 0 + ("8/8/8/2PPP3/2PKP3/2PPP3/8/8 w - - 0 1", 0, "surrounded by own"), + + # all eight adjacent squares are BLACK pieces → 8 (all captures) + ("8/8/8/2ppp3/2pKp3/2ppp3/8/8 w - - 0 1", 8, "surrounded by enemies"), + + # mixed: own block east/west; captures NE & SE; rest quiet + ("8/8/8/2p5/2PKP3/4p3/8/8 w - - 0 1", 6, "d4: caps c5,e3; quiet d5,d3,c3,e5"), + ] + + cases_king_caps = [ + ("8/8/8/8/3K4/8/8/8 w - - 0 1", 0, "no enemies"), + + # eight adjacent enemies → 8 captures + ("8/8/8/2ppp3/2pKp3/2ppp3/8/8 w - - 0 1", 8, "all adjacent caps"), + + # edge a1 with only b2 enemy → 1 capture + ("8/8/8/8/8/8/1p6/K7 w - - 0 1", 1, "a1: only Kxb2"), + + # mixed: same as above mixed case → 2 captures + ("8/8/8/2p5/2PKP3/4p3/8/8 w - - 0 1", 2, "captures c5 & e3"), + ] + + cases_king_castle_white = [ + ("8/8/8/8/8/8/8/R3K2R w KQ - 0 1", 7, "e1 with KQ rights, empty lanes: 5 king steps + O-O + O-O-O"), + ("8/8/8/8/8/8/8/4K2R w K - 0 1", 6, "e1 with K only: 5 steps + O-O"), + ("8/8/8/8/8/8/8/R3K3 w Q - 0 1", 6, "e1 with Q only: 5 steps + O-O-O"), + + # blocked kingside (f1 occupied) → only O-O-O available + ("8/8/8/8/8/8/8/R3KB1R w KQ - 0 1", 5, "f1 blocked by own piece; queen-side castle only"), + + # blocked queenside (d1 occupied) → only O-O available + ("8/8/8/8/8/8/8/R2BK2R w KQ - 0 1", 5, "d1 blocked by own piece; king-side castle only"), + + # both sides blocked (d1 and f1 occupied) → no castles, just 5 normal moves + ("8/8/8/8/8/8/8/R2BKB1R w KQ - 0 1", 3, "both lanes blocked; no castles"), + ] + + cases_king_castle_black = [ + ("r3k2r/8/8/8/8/8/8/8 b kq - 0 1", 7, "e8 with kq rights, empty lanes: 5 steps + O-O + O-O-O"), + ("4k2r/8/8/8/8/8/8/8 b k - 0 1", 6, "e8 with k only: 5 steps + O-O"), + ("r3k3/8/8/8/8/8/8/8 b q - 0 1", 6, "e8 with q only: 5 steps + O-O-O"), + + # blocked kingside (f8 occupied) → only O-O-O available + ("r3kb1r/8/8/8/8/8/8/8 b kq - 0 1", 5, "f8 blocked; only queen-side castle"), + + # 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) + + + def test_quiet_pawn_pushes_white(self): + cases = [ + ("8/pppppppp/8/8/8/8/PPPPPPPP/8 w - - 0 1", 16, "open ranks"), + ("8/8/8/8/8/8/P7/8 w - - 0 1", 2, "single pawn open"), + ("8/8/8/8/8/p7/P7/8 w - - 0 1", 0, "blocked single enemy"), + ("8/8/8/8/8/n7/P7/8 w - - 0 1", 0, "blocked single friendly"), + # 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) + + + def test_quiet_pawn_promotions_white(self): + cases = [ + ("8/PPPPPPPP/8/8/8/8/8/8 w - - 0 1", 8, "all open on 7th rank"), + ("8/8/7P/8/8/8/8/8 w - - 0 1", 0, "no promotions"), + ("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) + + + def test_capture_pawn_promotions_white(self): + cases = [ + ("6n1/7P/8/8/8/8/8/8 w - - 0 1", 1, "one capture"), + ("5n1n/6P1/8/8/8/8/8/8 w - - 0 1", 2, "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) + + + def test_capture_pawn_white(self): + cases = [ + # normal diagonal captures + ("8/8/8/3n4/4P3/8/8/8 w - - 0 1", 1, "e4xd5"), + ("8/8/8/3n1n2/4P3/8/8/8 w - - 0 1", 2, "e4xd5 and e4xf5"), + ("8/8/8/1p6/P7/8/8/8 w - - 0 1", 1, "edge file: a4xb5"), + ("8/7P/8/8/8/8/8/8 w - - 0 1", 0, "no capture"), + + # en passant + ("8/8/8/3pP3/8/8/8/8 w - d6 0 1", 1, "EP available: e5xd6 e.p. (black pawn on d5)"), + ("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 diff --git a/test/test_piece_attack_cache.py b/test/test_piece_attack_cache.py index 0611506..25ad432 100644 --- a/test/test_piece_attack_cache.py +++ b/test/test_piece_attack_cache.py @@ -1,8 +1,8 @@ from test.base import ChessLibTestBase -from test.base import bb_from -from test.base import draw_bb -from test.base import sq -from test.base import BLACK, WHITE +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 class KnightFixedCases(ChessLibTestBase):