From a31b3ca265907abf52965e6cf41e2fdb763971db Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 14:09:42 -0400 Subject: [PATCH 01/17] Add move type enum and move struct --- engine/include/bitboard.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/engine/include/bitboard.h b/engine/include/bitboard.h index c2ac730..35f7feb 100644 --- a/engine/include/bitboard.h +++ b/engine/include/bitboard.h @@ -38,6 +38,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 +72,5 @@ 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(); +int gen_pseudo_moves(const struct Board *b, Move *out, bool captures_only); \ No newline at end of file -- 2.34.1 From 71f38b3f3e2febcb713461e6d85527878ec76cda Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 14:14:47 -0400 Subject: [PATCH 02/17] Fix struct --- engine/include/bitboard.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/include/bitboard.h b/engine/include/bitboard.h index 35f7feb..7ac5118 100644 --- a/engine/include/bitboard.h +++ b/engine/include/bitboard.h @@ -53,7 +53,7 @@ struct Move { 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. @@ -73,4 +73,4 @@ void create_knight_attack_cache(); void create_pawn_attack_cache(); void create_king_attack_cache(); void print_board(); -int gen_pseudo_moves(const struct Board *b, Move *out, bool captures_only); \ No newline at end of file +int gen_pseudo_moves(struct Board *b, struct Move *out, bool captures_only); \ No newline at end of file -- 2.34.1 From ed533736e6d7891c4483e0ce4b0d425edb51d17e Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 14:14:58 -0400 Subject: [PATCH 03/17] Add diagonal and orthogonal movements --- engine/src/bitboard.c | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index f34239c..c8cad1c 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -76,6 +76,53 @@ static int pop_lsb_index(uint64_t *bb) { return idx; } +static inline 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; +} + +static inline 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 +} + +static inline 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 +} + +static inline 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 print_board(const struct Board *b) { static const char PIECE_CH[12] = { 'P','N','B','R','Q','K', -- 2.34.1 From e9d64a35dbbf54c4e86cdc8269bb0ae9831b9b89 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:06:18 -0400 Subject: [PATCH 04/17] Add king castle helpers --- engine/include/bitboard.h | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/engine/include/bitboard.h b/engine/include/bitboard.h index 7ac5118..3d940a8 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 -- 2.34.1 From 9c9dedc7bfc955e9a3cda424b108bca8e3eabced Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:07:13 -0400 Subject: [PATCH 05/17] Add non pawn movement generators --- engine/src/bitboard.c | 179 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index c8cad1c..8959818 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -76,7 +76,7 @@ static int pop_lsb_index(uint64_t *bb) { return idx; } -static inline uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, uint64_t occupied) +static uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, uint64_t occupied) { uint64_t attacks = 0; @@ -104,25 +104,196 @@ static inline uint64_t ray_attacks(int start_square, int delta_file, int delta_r return attacks; } -static inline uint64_t rook_attacks(int square, uint64_t occupied) { +static 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 } -static inline uint64_t bishop_attacks(int square, uint64_t occupied) { +static 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 } -static inline uint64_t queen_attacks(int square, uint64_t occupied) { +static uint64_t queen_attacks(int square, uint64_t occupied) { // Simply combine both types of attacks return rook_attacks(square, occupied) | bishop_attacks(square, occupied); } +static void push_move(struct Move *out, int *count, int from, int to, uint8_t piece, uint8_t promo, uint8_t flags) +{ + out[*count] = (Move){ + .from = (uint16_t)from, + .to = (uint16_t)to, + .piece = piece, + .promo = promo, // 0 for non-promo; + .flags = flags + }; + (*count)++; +} + + +/** + + + Pieces that are not PAWNs + + + */ +static void gen_knight_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { + enum Color side = b->side_to_move; + uint64_t own = b->occ[side]; + uint64_t opp = b->occ[side ^ 1]; + uint8_t pid = (side == WHITE) ? N : n; + uint64_t bb = (side == WHITE) ? b->pieces[N] : b->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, n, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, n, from, to, pid, 0, MF_CAPTURE); + } + } +} + +static void gen_bishop_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { + enum Color side = b->side_to_move; + uint64_t own = b->occ[side]; + uint64_t opp = b->occ[side ^ 1]; + uint64_t occ = b->occ[BOTH]; + uint8_t pid = (side == WHITE) ? B : b; + uint64_t bb = (side == WHITE) ? b->pieces[B] : b->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, n, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, n, from, to, pid, 0, MF_CAPTURE); + } + } +} + +static void gen_rook_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { + enum Color side = b->side_to_move; + uint64_t own = b->occ[side]; + uint64_t opp = b->occ[side ^ 1]; + uint64_t occ = b->occ[BOTH]; + uint8_t pid = (side == WHITE) ? R : r; + uint64_t bb = (side == WHITE) ? b->pieces[R] : b->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, n, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, n, from, to, pid, 0, MF_CAPTURE); + } + } +} + +static void gen_queen_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { + enum Color side = b->side_to_move; + uint64_t own = b->occ[side]; + uint64_t opp = b->occ[side ^ 1]; + uint64_t occ = b->occ[BOTH]; + uint8_t pid = (side == WHITE) ? Q : q; + uint64_t bb = (side == WHITE) ? b->pieces[Q] : b->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, n, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, n, from, to, pid, 0, MF_CAPTURE); + } + } +} + +static void gen_king_moves(struct Board *b, struct Move *out, int *n, bool captures_only) { + enum Color side = b->side_to_move; + uint64_t own = b->occ[side]; + uint64_t opp = b->occ[side ^ 1]; + uint8_t pid = (side == WHITE) ? K : k; + uint64_t kk = (side == WHITE) ? b->pieces[K] : b->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, n, from, to, pid, 0, MF_NONE); + } + } + while (caps) { + int to = pop_lsb_index(&caps); + push_move(out, n, from, to, pid, 0, MF_CAPTURE); + } + + if (!captures_only) { + uint64_t occ = b->occ[BOTH]; + if (side == WHITE) { + if ((b->castling_rights & CASTLE_WK) && !(occ & WK_EMPTY_MASK)) { + push_move(out, n, E1, WK_TO, K, 0, MF_CASTLE); + } + if ((b->castling_rights & CASTLE_WQ) && !(occ & WQ_EMPTY_MASK)) { + push_move(out, n, E1, WQ_TO, K, 0, MF_CASTLE); + } + } else { + if ((b->castling_rights & CASTLE_BK) && !(occ & BK_EMPTY_MASK)) { + push_move(out, n, E8, BK_TO, k, 0, MF_CASTLE); + } + if ((b->castling_rights & CASTLE_BQ) && !(occ & BQ_EMPTY_MASK)) { + push_move(out, n, E8, BQ_TO, k, 0, MF_CASTLE); + } + } + } +} + void print_board(const struct Board *b) { static const char PIECE_CH[12] = { 'P','N','B','R','Q','K', -- 2.34.1 From d54168432227ee0311c2d996852138bdcff7ab70 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:07:43 -0400 Subject: [PATCH 06/17] Add pawn movement generators Pawns are complicated pieces and have multiple cases to account for. Additionally, black and white pawns move in opposite ways to each other. We separated out the two colors to help keep the code clearer at a higher level, and to hopefully help with debugging in the future. --- engine/src/bitboard.c | 208 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index 8959818..31c77b9 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -135,6 +135,214 @@ static void push_move(struct Move *out, int *count, int from, int to, uint8_t pi (*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. +*/ +static void gen_white_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *n) { + const uint64_t occ = b->occ[BOTH]; + const uint64_t empty = ~occ; + + const uint64_t pawns = b->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, n, from, to, P, 0, MF_NONE); + } + + bb = two_step; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to - 16; + push_move(out, n, from, to, P, 0, MF_DOUBLE_PUSH); + } +} + +static void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *n) { + const uint64_t occ = b->occ[BOTH]; + const uint64_t empty = ~occ; + + const uint64_t pawns = b->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, n, from, to, p, 0, MF_NONE); + } + + bb = two_step; + while (bb) { + int to = pop_lsb_index(&bb); + int from = to + 16; + push_move(out, n, 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. +static void gen_white_pawn_push_promotions(const struct Board *b, struct Move *out, int *n) { + const uint64_t occ = b->occ[BOTH]; + const uint64_t empty = ~occ; + const uint64_t pawns = b->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, n, from, to, P, Q, MF_PROMO); + } +} + +static void gen_black_pawn_push_promotions(const struct Board *b, struct Move *out, int *n) { + const uint64_t occ = b->occ[BOTH]; + const uint64_t empty = ~occ; + const uint64_t pawns = b->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, n, from, to, p, q, MF_PROMO); + } +} + +static void gen_white_pawn_capture_promotions(const struct Board *b, struct Move *out, int *n) { + const uint64_t opp = b->occ[BLACK]; + // left capture (from white view): +7, mask off file A + uint64_t left = ((b->pieces[P] & ~FILE_A) << 7) & opp & RANK_8; + while (left) { + int to = pop_lsb_index(&left); + int from = to - 7; + push_move(out, n, from, to, P, Q, MF_CAPTURE | MF_PROMO); + } + // right capture: +9, mask off file H + uint64_t right = ((b->pieces[P] & ~FILE_H) << 9) & opp & RANK_8; + while (right) { + int to = pop_lsb_index(&right); + int from = to - 9; + push_move(out, n, from, to, P, Q, MF_CAPTURE | MF_PROMO); + } +} + +static void gen_black_pawn_capture_promotions(const struct Board *b, struct Move *out, int *n) { + const uint64_t opp = b->occ[WHITE]; + // from black view, “left” is -7 (mask off file H before shifting) + uint64_t left = ((b->pieces[p] & ~FILE_H) >> 7) & opp & RANK_1; + while (left) { + int to = pop_lsb_index(&left); + int from = to + 7; + push_move(out, n, from, to, p, q, MF_CAPTURE | MF_PROMO); + } + // “right” is -9 (mask off file A) + uint64_t right = ((b->pieces[p] & ~FILE_A) >> 9) & opp & RANK_1; + while (right) { + int to = pop_lsb_index(&right); + int from = to + 9; + push_move(out, n, from, to, p, q, MF_CAPTURE | MF_PROMO); + } +} + +static void gen_white_pawn_captures(const struct Board *b, struct Move *out, int *n) { + const uint64_t pawns = b->pieces[P]; + const uint64_t opp = b->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, n, from, to, P, 0, MF_CAPTURE); + } + while (right_caps) { + int to = pop_lsb_index(&right_caps); + int from = to - 9; + push_move(out, n, from, to, P, 0, MF_CAPTURE); + } + + // En passant (destination is ep_square) + if (b->ep_square >= 0) { + uint64_t ep = 1ULL << b->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 = b->ep_square; + // EP never promotes, so no promo piece; still a capture + push_move(out, n, from, to, P, 0, MF_CAPTURE | MF_ENPASSANT); + } + } +} + +static void gen_black_pawn_captures(const struct Board *b, struct Move *out, int *n) { + const uint64_t pawns = b->pieces[p]; + const uint64_t opp = b->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, n, from, to, p, 0, MF_CAPTURE); + } + while (right_caps) { + int to = pop_lsb_index(&right_caps); + int from = to + 9; + push_move(out, n, from, to, p, 0, MF_CAPTURE); + } + + // En passant + if (b->ep_square >= 0) { + uint64_t ep = 1ULL << b->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 = b->ep_square; + push_move(out, n, from, to, p, 0, MF_CAPTURE | MF_ENPASSANT); + } + } +} /** -- 2.34.1 From c37c85d99e5ac0d1de3c93aba8c440a4f7688b39 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:23:18 -0400 Subject: [PATCH 07/17] Remove variable naming conflicts --- engine/src/bitboard.c | 114 +++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index 31c77b9..934625b 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,7 +76,7 @@ static int pop_lsb_index(uint64_t *bb) { return idx; } -static uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, uint64_t occupied) +uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, uint64_t occupied) { uint64_t attacks = 0; @@ -104,28 +104,28 @@ static uint64_t ray_attacks(int start_square, int delta_file, int delta_rank, ui return attacks; } -static uint64_t rook_attacks(int square, uint64_t occupied) { +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 } -static uint64_t bishop_attacks(int square, uint64_t occupied) { +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 } -static uint64_t queen_attacks(int square, uint64_t occupied) { +uint64_t queen_attacks(int square, uint64_t occupied) { // Simply combine both types of attacks return rook_attacks(square, occupied) | bishop_attacks(square, occupied); } -static void push_move(struct Move *out, int *count, int from, int to, uint8_t piece, uint8_t promo, uint8_t flags) +void push_move(struct Move *out, int *count, int from, int to, uint8_t piece, uint8_t promo, uint8_t flags) { - out[*count] = (Move){ + out[*count] = (struct Move){ .from = (uint16_t)from, .to = (uint16_t)to, .piece = piece, @@ -146,7 +146,7 @@ static void push_move(struct Move *out, int *count, int from, int to, uint8_t pi most complicated movement patterns and behaviors in the game. Separating out the logic should hopefully make debugging easier. */ -static void gen_white_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *n) { +void gen_white_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *count) { const uint64_t occ = b->occ[BOTH]; const uint64_t empty = ~occ; @@ -166,18 +166,18 @@ static void gen_white_pawn_quiet_pushes(const struct Board *b, struct Move *out, while (bb) { int to = pop_lsb_index(&bb); int from = to - 8; - push_move(out, n, from, to, P, 0, MF_NONE); + 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, n, from, to, P, 0, MF_DOUBLE_PUSH); + push_move(out, count, from, to, P, 0, MF_DOUBLE_PUSH); } } -static void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *n) { +void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *count) { const uint64_t occ = b->occ[BOTH]; const uint64_t empty = ~occ; @@ -195,20 +195,20 @@ static void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, while (bb) { int to = pop_lsb_index(&bb); int from = to + 8; - push_move(out, n, from, to, p, 0, MF_NONE); + 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, n, from, to, p, 0, MF_DOUBLE_PUSH); + 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. -static void gen_white_pawn_push_promotions(const struct Board *b, struct Move *out, int *n) { +void gen_white_pawn_push_promotions(const struct Board *b, struct Move *out, int *count) { const uint64_t occ = b->occ[BOTH]; const uint64_t empty = ~occ; const uint64_t pawns = b->pieces[P]; @@ -219,11 +219,11 @@ static void gen_white_pawn_push_promotions(const struct Board *b, struct Move *o while (promos) { int to = pop_lsb_index(&promos); int from = to - 8; - push_move(out, n, from, to, P, Q, MF_PROMO); + push_move(out, count, from, to, P, Q, MF_PROMO); } } -static void gen_black_pawn_push_promotions(const struct Board *b, struct Move *out, int *n) { +void gen_black_pawn_push_promotions(const struct Board *b, struct Move *out, int *count) { const uint64_t occ = b->occ[BOTH]; const uint64_t empty = ~occ; const uint64_t pawns = b->pieces[p]; @@ -234,47 +234,47 @@ static void gen_black_pawn_push_promotions(const struct Board *b, struct Move *o while (promos) { int to = pop_lsb_index(&promos); int from = to + 8; - push_move(out, n, from, to, p, q, MF_PROMO); + push_move(out, count, from, to, p, q, MF_PROMO); } } -static void gen_white_pawn_capture_promotions(const struct Board *b, struct Move *out, int *n) { +void gen_white_pawn_capture_promotions(const struct Board *b, struct Move *out, int *count) { const uint64_t opp = b->occ[BLACK]; // left capture (from white view): +7, mask off file A uint64_t left = ((b->pieces[P] & ~FILE_A) << 7) & opp & RANK_8; while (left) { int to = pop_lsb_index(&left); int from = to - 7; - push_move(out, n, from, to, P, Q, MF_CAPTURE | MF_PROMO); + push_move(out, count,from, to, P, Q, MF_CAPTURE | MF_PROMO); } // right capture: +9, mask off file H uint64_t right = ((b->pieces[P] & ~FILE_H) << 9) & opp & RANK_8; while (right) { int to = pop_lsb_index(&right); int from = to - 9; - push_move(out, n, from, to, P, Q, MF_CAPTURE | MF_PROMO); + push_move(out, count,from, to, P, Q, MF_CAPTURE | MF_PROMO); } } -static void gen_black_pawn_capture_promotions(const struct Board *b, struct Move *out, int *n) { +void gen_black_pawn_capture_promotions(const struct Board *b, struct Move *out, int *count) { const uint64_t opp = b->occ[WHITE]; // from black view, “left” is -7 (mask off file H before shifting) uint64_t left = ((b->pieces[p] & ~FILE_H) >> 7) & opp & RANK_1; while (left) { int to = pop_lsb_index(&left); int from = to + 7; - push_move(out, n, from, to, p, q, MF_CAPTURE | MF_PROMO); + push_move(out, count,from, to, p, q, MF_CAPTURE | MF_PROMO); } // “right” is -9 (mask off file A) uint64_t right = ((b->pieces[p] & ~FILE_A) >> 9) & opp & RANK_1; while (right) { int to = pop_lsb_index(&right); int from = to + 9; - push_move(out, n, from, to, p, q, MF_CAPTURE | MF_PROMO); + push_move(out, count,from, to, p, q, MF_CAPTURE | MF_PROMO); } } -static void gen_white_pawn_captures(const struct Board *b, struct Move *out, int *n) { +void gen_white_pawn_captures(const struct Board *b, struct Move *out, int *count) { const uint64_t pawns = b->pieces[P]; const uint64_t opp = b->occ[BLACK]; @@ -286,12 +286,12 @@ static void gen_white_pawn_captures(const struct Board *b, struct Move *out, int while (left_caps) { int to = pop_lsb_index(&left_caps); int from = to - 7; - push_move(out, n, from, to, P, 0, MF_CAPTURE); + 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, n, from, to, P, 0, MF_CAPTURE); + push_move(out, count,from, to, P, 0, MF_CAPTURE); } // En passant (destination is ep_square) @@ -305,12 +305,12 @@ static void gen_white_pawn_captures(const struct Board *b, struct Move *out, int int from = pop_lsb_index(&ep_from); int to = b->ep_square; // EP never promotes, so no promo piece; still a capture - push_move(out, n, from, to, P, 0, MF_CAPTURE | MF_ENPASSANT); + push_move(out, count,from, to, P, 0, MF_CAPTURE | MF_ENPASSANT); } } } -static void gen_black_pawn_captures(const struct Board *b, struct Move *out, int *n) { +void gen_black_pawn_captures(const struct Board *b, struct Move *out, int *count) { const uint64_t pawns = b->pieces[p]; const uint64_t opp = b->occ[WHITE]; @@ -321,12 +321,12 @@ static void gen_black_pawn_captures(const struct Board *b, struct Move *out, int while (left_caps) { int to = pop_lsb_index(&left_caps); int from = to + 7; - push_move(out, n, from, to, p, 0, MF_CAPTURE); + 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, n, from, to, p, 0, MF_CAPTURE); + push_move(out, count,from, to, p, 0, MF_CAPTURE); } // En passant @@ -339,7 +339,7 @@ static void gen_black_pawn_captures(const struct Board *b, struct Move *out, int while (ep_from) { int from = pop_lsb_index(&ep_from); int to = b->ep_square; - push_move(out, n, from, to, p, 0, MF_CAPTURE | MF_ENPASSANT); + push_move(out, count,from, to, p, 0, MF_CAPTURE | MF_ENPASSANT); } } } @@ -351,7 +351,7 @@ static void gen_black_pawn_captures(const struct Board *b, struct Move *out, int */ -static void gen_knight_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { +void gen_knight_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { enum Color side = b->side_to_move; uint64_t own = b->occ[side]; uint64_t opp = b->occ[side ^ 1]; @@ -367,23 +367,23 @@ static void gen_knight_moves(const struct Board *b, struct Move *out, int *n, bo uint64_t quiet = mask & ~opp; while (quiet) { int to = pop_lsb_index(&quiet); - push_move(out, n, from, to, pid, 0, MF_NONE); + push_move(out, count, from, to, pid, 0, MF_NONE); } } while (caps) { int to = pop_lsb_index(&caps); - push_move(out, n, from, to, pid, 0, MF_CAPTURE); + push_move(out, count, from, to, pid, 0, MF_CAPTURE); } } } -static void gen_bishop_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { - enum Color side = b->side_to_move; - uint64_t own = b->occ[side]; - uint64_t opp = b->occ[side ^ 1]; - uint64_t occ = b->occ[BOTH]; +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) ? b->pieces[B] : b->pieces[b]; + uint64_t bb = (side == WHITE) ? board->pieces[B] : board->pieces[b]; while (bb) { int from = pop_lsb_index(&bb); @@ -394,17 +394,17 @@ static void gen_bishop_moves(const struct Board *b, struct Move *out, int *n, bo uint64_t quiet = mask & ~opp; while (quiet) { int to = pop_lsb_index(&quiet); - push_move(out, n, from, to, pid, 0, MF_NONE); + push_move(out, count, from, to, pid, 0, MF_NONE); } } while (caps) { int to = pop_lsb_index(&caps); - push_move(out, n, from, to, pid, 0, MF_CAPTURE); + push_move(out, count, from, to, pid, 0, MF_CAPTURE); } } } -static void gen_rook_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { +void gen_rook_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { enum Color side = b->side_to_move; uint64_t own = b->occ[side]; uint64_t opp = b->occ[side ^ 1]; @@ -421,17 +421,17 @@ static void gen_rook_moves(const struct Board *b, struct Move *out, int *n, bool uint64_t quiet = mask & ~opp; while (quiet) { int to = pop_lsb_index(&quiet); - push_move(out, n, from, to, pid, 0, MF_NONE); + push_move(out, count,from, to, pid, 0, MF_NONE); } } while (caps) { int to = pop_lsb_index(&caps); - push_move(out, n, from, to, pid, 0, MF_CAPTURE); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); } } } -static void gen_queen_moves(const struct Board *b, struct Move *out, int *n, bool captures_only) { +void gen_queen_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { enum Color side = b->side_to_move; uint64_t own = b->occ[side]; uint64_t opp = b->occ[side ^ 1]; @@ -448,17 +448,17 @@ static void gen_queen_moves(const struct Board *b, struct Move *out, int *n, boo uint64_t quiet = mask & ~opp; while (quiet) { int to = pop_lsb_index(&quiet); - push_move(out, n, from, to, pid, 0, MF_NONE); + push_move(out, count,from, to, pid, 0, MF_NONE); } } while (caps) { int to = pop_lsb_index(&caps); - push_move(out, n, from, to, pid, 0, MF_CAPTURE); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); } } } -static void gen_king_moves(struct Board *b, struct Move *out, int *n, bool captures_only) { +void gen_king_moves(struct Board *b, struct Move *out, int *count, bool captures_only) { enum Color side = b->side_to_move; uint64_t own = b->occ[side]; uint64_t opp = b->occ[side ^ 1]; @@ -474,36 +474,36 @@ static void gen_king_moves(struct Board *b, struct Move *out, int *n, bool captu uint64_t quiet = mask & ~opp; while (quiet) { int to = pop_lsb_index(&quiet); - push_move(out, n, from, to, pid, 0, MF_NONE); + push_move(out, count,from, to, pid, 0, MF_NONE); } } while (caps) { int to = pop_lsb_index(&caps); - push_move(out, n, from, to, pid, 0, MF_CAPTURE); + push_move(out, count,from, to, pid, 0, MF_CAPTURE); } if (!captures_only) { uint64_t occ = b->occ[BOTH]; if (side == WHITE) { if ((b->castling_rights & CASTLE_WK) && !(occ & WK_EMPTY_MASK)) { - push_move(out, n, E1, WK_TO, K, 0, MF_CASTLE); + push_move(out, count,E1, WK_TO, K, 0, MF_CASTLE); } if ((b->castling_rights & CASTLE_WQ) && !(occ & WQ_EMPTY_MASK)) { - push_move(out, n, E1, WQ_TO, K, 0, MF_CASTLE); + push_move(out, count,E1, WQ_TO, K, 0, MF_CASTLE); } } else { if ((b->castling_rights & CASTLE_BK) && !(occ & BK_EMPTY_MASK)) { - push_move(out, n, E8, BK_TO, k, 0, MF_CASTLE); + push_move(out, count,E8, BK_TO, k, 0, MF_CASTLE); } if ((b->castling_rights & CASTLE_BQ) && !(occ & BQ_EMPTY_MASK)) { - push_move(out, n, E8, BQ_TO, k, 0, MF_CASTLE); + push_move(out, count,E8, BQ_TO, k, 0, MF_CASTLE); } } } } void print_board(const struct Board *b) { - static const char PIECE_CH[12] = { + const char PIECE_CH[12] = { 'P','N','B','R','Q','K', 'p','n','b','r','q','k' }; -- 2.34.1 From 55666004a0538f8b6b07001c0a44fbe65cc93835 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:24:24 -0400 Subject: [PATCH 08/17] Rename b to board for clarity and consistency --- engine/src/bitboard.c | 122 +++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index 934625b..bc047bd 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -146,11 +146,11 @@ void push_move(struct Move *out, int *count, int from, int to, uint8_t piece, ui 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 *b, struct Move *out, int *count) { - const uint64_t occ = b->occ[BOTH]; +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 = b->pieces[P]; + 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; @@ -177,11 +177,11 @@ void gen_white_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *c } } -void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *count) { - const uint64_t occ = b->occ[BOTH]; +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 = b->pieces[p]; + 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; @@ -208,10 +208,10 @@ void gen_black_pawn_quiet_pushes(const struct Board *b, struct Move *out, int *c // 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 *b, struct Move *out, int *count) { - const uint64_t occ = b->occ[BOTH]; +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 = b->pieces[P]; + const uint64_t pawns = board->pieces[P]; // destinations on rank 8 reachable by a single push uint64_t promos = ((pawns << 8) & empty) & RANK_8; @@ -223,10 +223,10 @@ void gen_white_pawn_push_promotions(const struct Board *b, struct Move *out, int } } -void gen_black_pawn_push_promotions(const struct Board *b, struct Move *out, int *count) { - const uint64_t occ = b->occ[BOTH]; +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 = b->pieces[p]; + const uint64_t pawns = board->pieces[p]; // destinations on rank 1 reachable by a single push uint64_t promos = ((pawns >> 8) & empty) & RANK_1; @@ -238,17 +238,17 @@ void gen_black_pawn_push_promotions(const struct Board *b, struct Move *out, int } } -void gen_white_pawn_capture_promotions(const struct Board *b, struct Move *out, int *count) { - const uint64_t opp = b->occ[BLACK]; +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 = ((b->pieces[P] & ~FILE_A) << 7) & opp & RANK_8; + 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 = ((b->pieces[P] & ~FILE_H) << 9) & opp & RANK_8; + uint64_t right = ((board->pieces[P] & ~FILE_H) << 9) & opp & RANK_8; while (right) { int to = pop_lsb_index(&right); int from = to - 9; @@ -256,17 +256,17 @@ void gen_white_pawn_capture_promotions(const struct Board *b, struct Move *out, } } -void gen_black_pawn_capture_promotions(const struct Board *b, struct Move *out, int *count) { - const uint64_t opp = b->occ[WHITE]; +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 = ((b->pieces[p] & ~FILE_H) >> 7) & opp & RANK_1; + 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 = ((b->pieces[p] & ~FILE_A) >> 9) & opp & RANK_1; + uint64_t right = ((board->pieces[p] & ~FILE_A) >> 9) & opp & RANK_1; while (right) { int to = pop_lsb_index(&right); int from = to + 9; @@ -274,9 +274,9 @@ void gen_black_pawn_capture_promotions(const struct Board *b, struct Move *out, } } -void gen_white_pawn_captures(const struct Board *b, struct Move *out, int *count) { - const uint64_t pawns = b->pieces[P]; - const uint64_t opp = b->occ[BLACK]; +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; @@ -295,24 +295,24 @@ void gen_white_pawn_captures(const struct Board *b, struct Move *out, int *count } // En passant (destination is ep_square) - if (b->ep_square >= 0) { - uint64_t ep = 1ULL << b->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 = b->ep_square; + 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 *b, struct Move *out, int *count) { - const uint64_t pawns = b->pieces[p]; - const uint64_t opp = b->occ[WHITE]; +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 @@ -330,15 +330,15 @@ void gen_black_pawn_captures(const struct Board *b, struct Move *out, int *count } // En passant - if (b->ep_square >= 0) { - uint64_t ep = 1ULL << b->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_A) << 7) | ((ep & ~FILE_H) << 9)) & pawns; while (ep_from) { int from = pop_lsb_index(&ep_from); - int to = b->ep_square; + int to = board->ep_square; push_move(out, count,from, to, p, 0, MF_CAPTURE | MF_ENPASSANT); } } @@ -351,12 +351,12 @@ void gen_black_pawn_captures(const struct Board *b, struct Move *out, int *count */ -void gen_knight_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { - enum Color side = b->side_to_move; - uint64_t own = b->occ[side]; - uint64_t opp = b->occ[side ^ 1]; +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) ? b->pieces[N] : b->pieces[n]; + uint64_t bb = (side == WHITE) ? board->pieces[N] : board->pieces[n]; while (bb) { int from = pop_lsb_index(&bb); @@ -404,13 +404,13 @@ void gen_bishop_moves(const struct Board *board, struct Move *out, int *count, b } } -void gen_rook_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { - enum Color side = b->side_to_move; - uint64_t own = b->occ[side]; - uint64_t opp = b->occ[side ^ 1]; - uint64_t occ = b->occ[BOTH]; +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) ? b->pieces[R] : b->pieces[r]; + uint64_t bb = (side == WHITE) ? board->pieces[R] : board->pieces[r]; while (bb) { int from = pop_lsb_index(&bb); @@ -431,13 +431,13 @@ void gen_rook_moves(const struct Board *b, struct Move *out, int *count, bool ca } } -void gen_queen_moves(const struct Board *b, struct Move *out, int *count, bool captures_only) { - enum Color side = b->side_to_move; - uint64_t own = b->occ[side]; - uint64_t opp = b->occ[side ^ 1]; - uint64_t occ = b->occ[BOTH]; +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) ? b->pieces[Q] : b->pieces[q]; + uint64_t bb = (side == WHITE) ? board->pieces[Q] : board->pieces[q]; while (bb) { int from = pop_lsb_index(&bb); @@ -458,12 +458,12 @@ void gen_queen_moves(const struct Board *b, struct Move *out, int *count, bool c } } -void gen_king_moves(struct Board *b, struct Move *out, int *count, bool captures_only) { - enum Color side = b->side_to_move; - uint64_t own = b->occ[side]; - uint64_t opp = b->occ[side ^ 1]; +void gen_king_moves(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) ? b->pieces[K] : b->pieces[k]; + uint64_t kk = (side == WHITE) ? board->pieces[K] : board->pieces[k]; if (!kk) return; int from = first_set_index(kk); @@ -483,26 +483,26 @@ void gen_king_moves(struct Board *b, struct Move *out, int *count, bool captures } if (!captures_only) { - uint64_t occ = b->occ[BOTH]; + uint64_t occ = board->occ[BOTH]; if (side == WHITE) { - if ((b->castling_rights & CASTLE_WK) && !(occ & WK_EMPTY_MASK)) { + if ((board->castling_rights & CASTLE_WK) && !(occ & WK_EMPTY_MASK)) { push_move(out, count,E1, WK_TO, K, 0, MF_CASTLE); } - if ((b->castling_rights & CASTLE_WQ) && !(occ & WQ_EMPTY_MASK)) { + if ((board->castling_rights & CASTLE_WQ) && !(occ & WQ_EMPTY_MASK)) { push_move(out, count,E1, WQ_TO, K, 0, MF_CASTLE); } } else { - if ((b->castling_rights & CASTLE_BK) && !(occ & BK_EMPTY_MASK)) { + if ((board->castling_rights & CASTLE_BK) && !(occ & BK_EMPTY_MASK)) { push_move(out, count,E8, BK_TO, k, 0, MF_CASTLE); } - if ((b->castling_rights & CASTLE_BQ) && !(occ & BQ_EMPTY_MASK)) { + if ((board->castling_rights & CASTLE_BQ) && !(occ & BQ_EMPTY_MASK)) { push_move(out, count,E8, BQ_TO, k, 0, MF_CASTLE); } } } } -void print_board(const struct Board *b) { +void print_board(const struct Board *board) { const char PIECE_CH[12] = { 'P','N','B','R','Q','K', 'p','n','b','r','q','k' @@ -512,7 +512,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]; -- 2.34.1 From 3ca26064204fbe43a93ce41d1d3659131317082d Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:25:14 -0400 Subject: [PATCH 09/17] Fix comment --- engine/src/bitboard.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/bitboard.c b/engine/src/bitboard.c index bc047bd..a69f3bb 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -350,7 +350,7 @@ void gen_black_pawn_captures(const struct Board *board, struct Move *out, int *c 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]; -- 2.34.1 From 362048d63083bd94d4af8eab9d5e1eea39b11b94 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 Aug 2025 15:26:04 -0400 Subject: [PATCH 10/17] Fix formatting --- test/test_fen_loader.py | 2 ++ test/test_movegen.py | 0 2 files changed, 2 insertions(+) create mode 100644 test/test_movegen.py diff --git a/test/test_fen_loader.py b/test/test_fen_loader.py index 088b0ff..5bf3867 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -17,6 +17,8 @@ class Board(ctypes.Structure): ("halfmove_clock", ctypes.c_int), ("fullmove_number", ctypes.c_int), ] + + class FenTests(ChessLibTestBase): @classmethod def setUpClass(cls): diff --git a/test/test_movegen.py b/test/test_movegen.py new file mode 100644 index 0000000..e69de29 -- 2.34.1 From 5a25eba8f64598afad56a17f59ab99cf38dc6a8c Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 12:17:51 -0400 Subject: [PATCH 11/17] Add move_gen function --- engine/include/bitboard.h | 10 +++++++++- engine/src/bitboard.c | 31 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/engine/include/bitboard.h b/engine/include/bitboard.h index 3d940a8..acd9318 100644 --- a/engine/include/bitboard.h +++ b/engine/include/bitboard.h @@ -106,4 +106,12 @@ void create_knight_attack_cache(); void create_pawn_attack_cache(); void create_king_attack_cache(); void print_board(); -int gen_pseudo_moves(struct Board *b, struct Move *out, bool captures_only); \ No newline at end of file +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 a69f3bb..3583eb4 100644 --- a/engine/src/bitboard.c +++ b/engine/src/bitboard.c @@ -458,7 +458,7 @@ void gen_queen_moves(const struct Board *board, struct Move *out, int *count, bo } } -void gen_king_moves(struct Board *board, struct Move *out, int *count, bool captures_only) { +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]; @@ -502,6 +502,35 @@ void gen_king_moves(struct Board *board, struct Move *out, int *count, bool capt } } +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', -- 2.34.1 From 5b4dc7468191b03d377331d5fb4c66b2cbae6846 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 17:23:42 -0400 Subject: [PATCH 12/17] Update test suite Put all ffi code into its own module for clarity. --- test/base.py | 92 +++++-------------- test/chess_ffi.py | 158 ++++++++++++++++++++++++++++++++ test/test_fen_loader.py | 108 ++++++++++------------ test/test_movegen.py | 10 ++ test/test_piece_attack_cache.py | 8 +- 5 files changed, 242 insertions(+), 134 deletions(-) create mode 100644 test/chess_ffi.py diff --git a/test/base.py b/test/base.py index 5162eca..2d33202 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(self, fen: str): + rc = load_fen(self.board, fen) + self.assertEqual(rc, 0, f"load_fen failed rc={rc} for FEN: {fen}") + return self.board + + + 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 5bf3867..04a2758 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -1,83 +1,71 @@ -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 load_fen(self, fen, board=None): + if board: + return load_fen(board, fen) + return load_fen(self.board, fen) 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): @@ -88,8 +76,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) @@ -101,6 +88,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 index e69de29..6a71234 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -0,0 +1,10 @@ +import ctypes +from test.base import ChessLibTestBase + + + + +class TestMoveGeneration(ChessLibTestBase): + @classmethod + def setUpClass(cls): + pass 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): -- 2.34.1 From 2028d7fa121ad8a5e8aba32fc252ef927f4e556e Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 18:53:15 -0400 Subject: [PATCH 13/17] Add white pawn move gen test --- test/base.py | 8 ++-- test/test_fen_loader.py | 6 --- test/test_movegen.py | 97 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/test/base.py b/test/base.py index 2d33202..02b4d63 100644 --- a/test/base.py +++ b/test/base.py @@ -23,10 +23,10 @@ class ChessLibTestBase(unittest.TestCase): self.board = Board() - def load(self, fen: str): - rc = load_fen(self.board, fen) - self.assertEqual(rc, 0, f"load_fen failed rc={rc} for FEN: {fen}") - return self.board + 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): diff --git a/test/test_fen_loader.py b/test/test_fen_loader.py index 04a2758..37620b0 100644 --- a/test/test_fen_loader.py +++ b/test/test_fen_loader.py @@ -14,12 +14,6 @@ class TestFenLoading(ChessLibTestBase): return sum(1 << (r*8 + f) for f in range(8)) - def load_fen(self, fen, board=None): - if board: - return load_fen(board, fen) - return load_fen(self.board, fen) - - def test_startpos_fields_and_occupancies(self): fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" rc = self.load_fen(fen) diff --git a/test/test_movegen.py b/test/test_movegen.py index 6a71234..36048ca 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -1,10 +1,101 @@ 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 Move +from test.chess_ffi import Board +MAX_MOVES = 256 class TestMoveGeneration(ChessLibTestBase): - @classmethod - def setUpClass(cls): - pass + + + 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"), + ] + + 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) + gen_white_pawn_quiet_pushes(b, moves, ctypes.byref(cnt)) + + self.assertEqual(expected, cnt.value) + + + 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"), + ] + + 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) + gen_white_pawn_push_promotions(b, moves, ctypes.byref(cnt)) + + self.assertEqual(expected, cnt.value) + + + 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"), + ] + + 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) + gen_white_pawn_capture_promotions(b, moves, ctypes.byref(cnt)) + + self.assertEqual(expected, cnt.value) + + + 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"), + ] + + 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) + gen_white_pawn_captures(b, moves, ctypes.byref(cnt)) + + self.assertEqual(expected, cnt.value) \ No newline at end of file -- 2.34.1 From fb5e8bb0cbb5fe394ee9954465efa950c2f1fb60 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 19:02:00 -0400 Subject: [PATCH 14/17] Add knight tests --- test/test_movegen.py | 69 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/test/test_movegen.py b/test/test_movegen.py index 36048ca..dbbfccd 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -4,6 +4,7 @@ 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 Move from test.chess_ffi import Board @@ -11,7 +12,73 @@ from test.chess_ffi import Board MAX_MOVES = 256 -class TestMoveGeneration(ChessLibTestBase): +class TestPseudoMoveGeneration(ChessLibTestBase): + 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 = False + for fen, expected, msg in all_move_types: + with self.subTest(msg=msg, fen=fen): + cnt = ctypes.c_int(0) + moves = (Move * MAX_MOVES)() + b = Board() + + self.load_fen(fen, board=b) + gen_knight_moves(b, moves, ctypes.byref(cnt), captures_only) + + self.assertEqual(expected, cnt.value) + + cases_knight_captures = [ + ("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"), + ] + + captures_only = True + for fen, expected, msg in cases_knight_captures: + with self.subTest(msg=msg, fen=fen): + cnt = ctypes.c_int(0) + moves = (Move * MAX_MOVES)() + b = Board() + + self.load_fen(fen, board=b) + gen_knight_moves(b, moves, ctypes.byref(cnt), captures_only) + + self.assertEqual(expected, cnt.value) + + + def test_bishop_move_gen(self): + pass + + + def test_rook_move_gen(self): + pass + + + def test_queen_move_gen(self): + pass + + + def test_king_move_gen(self): + pass def test_quiet_pawn_pushes_white(self): -- 2.34.1 From af829f2208be7bcc19332b456afb6cdfc5f7b544 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 19:12:16 -0400 Subject: [PATCH 15/17] Add bishop move gen tests --- test/test_movegen.py | 56 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/test/test_movegen.py b/test/test_movegen.py index dbbfccd..f84ef4d 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -5,6 +5,7 @@ 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 Move from test.chess_ffi import Board @@ -15,7 +16,7 @@ MAX_MOVES = 256 class TestPseudoMoveGeneration(ChessLibTestBase): 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/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"), @@ -66,7 +67,58 @@ class TestPseudoMoveGeneration(ChessLibTestBase): def test_bishop_move_gen(self): - pass + 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, ""), + ] + + captures_only = False + for fen, expected, msg in cases_bishop_all: + with self.subTest(msg=msg, fen=fen): + cnt = ctypes.c_int(0) + moves = (Move * MAX_MOVES)() + b = Board() + + self.load_fen(fen, board=b) + gen_bishop_moves(b, moves, ctypes.byref(cnt), captures_only) + + self.assertEqual(expected, cnt.value) + + 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"), + ] + + captures_only = True + for fen, expected, msg in cases_bishop_caps: + with self.subTest(msg=msg, fen=fen): + cnt = ctypes.c_int(0) + moves = (Move * MAX_MOVES)() + b = Board() + + self.load_fen(fen, board=b) + gen_bishop_moves(b, moves, ctypes.byref(cnt), captures_only) + + self.assertEqual(expected, cnt.value) def test_rook_move_gen(self): -- 2.34.1 From fd9af926e0916f37513b5365a87408fefbe1edbc Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 19:50:17 -0400 Subject: [PATCH 16/17] Add rook move gen and refactor sub test checks --- test/test_movegen.py | 155 +++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 93 deletions(-) diff --git a/test/test_movegen.py b/test/test_movegen.py index f84ef4d..c8146ff 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -6,6 +6,7 @@ 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 Move from test.chess_ffi import Board @@ -14,6 +15,22 @@ 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"), @@ -34,36 +51,15 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("8/8/8/8/8/1p6/2P5/N7 w - - 0 1", 1, "a1, b3 black (capture), c2 white (blocked)"), ] - captures_only = False - for fen, expected, msg in all_move_types: - with self.subTest(msg=msg, fen=fen): - cnt = ctypes.c_int(0) - moves = (Move * MAX_MOVES)() - b = Board() - - self.load_fen(fen, board=b) - gen_knight_moves(b, moves, ctypes.byref(cnt), captures_only) - - self.assertEqual(expected, cnt.value) - - cases_knight_captures = [ + 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) - captures_only = True - for fen, expected, msg in cases_knight_captures: - with self.subTest(msg=msg, fen=fen): - cnt = ctypes.c_int(0) - moves = (Move * MAX_MOVES)() - b = Board() - - self.load_fen(fen, board=b) - gen_knight_moves(b, moves, ctypes.byref(cnt), captures_only) - - self.assertEqual(expected, cnt.value) def test_bishop_move_gen(self): @@ -83,18 +79,6 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("8/8/1p3P2/8/3B4/4p3/1p6/8 w - - 0 1", 6, ""), ] - captures_only = False - for fen, expected, msg in cases_bishop_all: - with self.subTest(msg=msg, fen=fen): - cnt = ctypes.c_int(0) - moves = (Move * MAX_MOVES)() - b = Board() - - self.load_fen(fen, board=b) - gen_bishop_moves(b, moves, ctypes.byref(cnt), captures_only) - - self.assertEqual(expected, cnt.value) - cases_bishop_caps = [ ("8/8/8/8/3B4/8/8/8 w - - 0 1", 0, "no enemies to capture"), @@ -107,22 +91,47 @@ 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"), ] - - captures_only = True - for fen, expected, msg in cases_bishop_caps: - with self.subTest(msg=msg, fen=fen): - cnt = ctypes.c_int(0) - moves = (Move * MAX_MOVES)() - b = Board() - - self.load_fen(fen, board=b) - gen_bishop_moves(b, moves, ctypes.byref(cnt), captures_only) - - self.assertEqual(expected, cnt.value) + 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): - pass + 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): @@ -142,17 +151,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"), ] - - 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) - gen_white_pawn_quiet_pushes(b, moves, ctypes.byref(cnt)) - - self.assertEqual(expected, cnt.value) + self.run_subtests(cases, gen_white_pawn_quiet_pushes) def test_quiet_pawn_promotions_white(self): @@ -162,17 +161,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"), ] - - 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) - gen_white_pawn_push_promotions(b, moves, ctypes.byref(cnt)) - - self.assertEqual(expected, cnt.value) + self.run_subtests(cases, gen_white_pawn_push_promotions) def test_capture_pawn_promotions_white(self): @@ -181,17 +170,7 @@ class TestPseudoMoveGeneration(ChessLibTestBase): ("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"), ] - - 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) - gen_white_pawn_capture_promotions(b, moves, ctypes.byref(cnt)) - - self.assertEqual(expected, cnt.value) + self.run_subtests(cases, gen_white_pawn_capture_promotions) def test_capture_pawn_white(self): @@ -207,14 +186,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"), ] - - 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) - gen_white_pawn_captures(b, moves, ctypes.byref(cnt)) - - self.assertEqual(expected, cnt.value) \ No newline at end of file + self.run_subtests(cases, gen_white_pawn_captures) \ No newline at end of file -- 2.34.1 From b811e33b8127788a6c84e7c2769b2f7ae2dd09f2 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 17 Aug 2025 20:27:03 -0400 Subject: [PATCH 17/17] Add king move gen and a test.c file for quick debug --- engine/src/test.c | 117 ++++++++++++++++++++++++++++++++++++------- makefile | 13 ++--- test/test_movegen.py | 103 +++++++++++++++++++++++++++++++++++-- 3 files changed, 207 insertions(+), 26 deletions(-) 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/test_movegen.py b/test/test_movegen.py index c8146ff..c809061 100644 --- a/test/test_movegen.py +++ b/test/test_movegen.py @@ -7,8 +7,12 @@ 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 @@ -61,7 +65,6 @@ class TestPseudoMoveGeneration(ChessLibTestBase): 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"), @@ -135,11 +138,105 @@ class TestPseudoMoveGeneration(ChessLibTestBase): def test_queen_move_gen(self): - pass + 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): - pass + 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): -- 2.34.1