mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:23:03 +02:00
1441 lines
40 KiB
C
1441 lines
40 KiB
C
|
|
/*
|
||
|
|
* test_movegen.c - Unit tests for movegen.c (parse_fen, make_move, unmake_move,
|
||
|
|
* gen_moves, in_check, square_from_algebraic, move_from_uci).
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include "movegen.h"
|
||
|
|
|
||
|
|
#include <assert.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
/* Helper: count moves matching a given to-square */
|
||
|
|
static int count_moves_to(Move *moves, int n, int to)
|
||
|
|
{
|
||
|
|
int c = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].to == to)
|
||
|
|
{
|
||
|
|
c++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return c;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Helper: find a move from→to in the move list; returns index or -1 */
|
||
|
|
static int find_move(Move *moves, int n, int from, int to)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == from && moves[i].to == to)
|
||
|
|
{
|
||
|
|
return i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* parse_fen tests
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_parse_fen_startpos(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||
|
|
assert(ok);
|
||
|
|
assert(pos.side == WHITE);
|
||
|
|
assert(pos.castle == 0xF); /* all four castling rights */
|
||
|
|
assert(pos.ep_square == -1);
|
||
|
|
assert(pos.halfmove_clock == 0);
|
||
|
|
assert(pos.fullmove_number == 1);
|
||
|
|
|
||
|
|
/* a1 = 0x00 should be WR */
|
||
|
|
assert(pos.board[0x00] == WR);
|
||
|
|
/* e1 = 0x04 WK */
|
||
|
|
assert(pos.board[0x04] == WK);
|
||
|
|
/* e8 = 0x74 BK */
|
||
|
|
assert(pos.board[0x74] == BK);
|
||
|
|
/* a2 = 0x10 WP */
|
||
|
|
assert(pos.board[0x10] == WP);
|
||
|
|
/* e5 = 0x44 EMPTY */
|
||
|
|
assert(pos.board[0x44] == EMPTY);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_black_to_move(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1");
|
||
|
|
assert(ok);
|
||
|
|
assert(pos.side == BLACK);
|
||
|
|
/* e3 = rank1+2, file e=4 => 0x24 */
|
||
|
|
assert(pos.ep_square == 0x24);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_no_castling(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1");
|
||
|
|
assert(ok);
|
||
|
|
assert(pos.castle == 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_partial_castling(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "r3k2r/8/8/8/8/8/8/R3K2R w Kq - 0 1");
|
||
|
|
assert(ok);
|
||
|
|
assert(pos.castle & (1 << 0)); /* white kingside */
|
||
|
|
assert(!(pos.castle & (1 << 1))); /* not white queenside */
|
||
|
|
assert(!(pos.castle & (1 << 2))); /* not black kingside */
|
||
|
|
assert(pos.castle & (1 << 3)); /* black queenside */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_invalid_missing_space(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_invalid_side(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_invalid_ep(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq z9 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_invalid_castling_char(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KX - 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_invalid_off_board(void)
|
||
|
|
{
|
||
|
|
/* Placing a piece on an off-board square (like rank 0 with EMPTY in between)
|
||
|
|
* Use a FEN where the piece placement lands on off-board sq: try a fen where
|
||
|
|
* piece placement skips the separator and goes past rank end. Since the parser
|
||
|
|
* only checks on_board when placing a piece character (not digits), we verify
|
||
|
|
* that placing AT an off-board square returns 0. This is tricky to trigger via
|
||
|
|
* a simple digit overflow, but we can use a malformed FEN where the piece
|
||
|
|
* is placed at sq 0x08 which is off-board: one rank only with 10 pieces. */
|
||
|
|
/* After A8-H8 (8 squares) sq=0x78 which is off-board. Place a 9th PIECE. */
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "RNBQKBNRP/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_parse_fen_fullmove_and_halfmove(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
int ok = parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 5 12");
|
||
|
|
assert(ok);
|
||
|
|
assert(pos.halfmove_clock == 5);
|
||
|
|
assert(pos.fullmove_number == 12);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_set_startpos(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
assert(pos.side == WHITE);
|
||
|
|
assert(pos.board[0x04] == WK);
|
||
|
|
assert(pos.board[0x74] == BK);
|
||
|
|
assert(pos.castle == 0xF);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* square_from_algebraic tests
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_square_from_algebraic_basic(void)
|
||
|
|
{
|
||
|
|
/* e2e4 -> from=e2=0x14, to=e4=0x34 */
|
||
|
|
assert(square_from_algebraic("e2e4", 1) == 0x14);
|
||
|
|
assert(square_from_algebraic("e2e4", 0) == 0x34);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_square_from_algebraic_promotion(void)
|
||
|
|
{
|
||
|
|
/* e7e8q: from e7=0x64, to e8=0x74 */
|
||
|
|
assert(square_from_algebraic("e7e8q", 1) == 0x64);
|
||
|
|
assert(square_from_algebraic("e7e8q", 0) == 0x74);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_square_from_algebraic_null(void)
|
||
|
|
{
|
||
|
|
assert(square_from_algebraic(NULL, 0) == -1);
|
||
|
|
assert(square_from_algebraic("e2", 0) == -1); /* too short */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_square_from_algebraic_a1(void)
|
||
|
|
{
|
||
|
|
assert(square_from_algebraic("a1b2", 1) == 0x00);
|
||
|
|
assert(square_from_algebraic("a1b2", 0) == 0x11);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_square_from_algebraic_h8(void)
|
||
|
|
{
|
||
|
|
assert(square_from_algebraic("h8g7", 1) == 0x77);
|
||
|
|
assert(square_from_algebraic("h8g7", 0) == 0x66);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* make_move / unmake_move tests
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_make_unmake_pawn_push(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
|
||
|
|
/* e2e4 */
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e2e4", &m);
|
||
|
|
assert(found);
|
||
|
|
|
||
|
|
Position before = pos;
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
|
||
|
|
assert(cap == EMPTY);
|
||
|
|
assert(pos.board[0x14] == EMPTY);
|
||
|
|
assert(pos.board[0x34] == WP);
|
||
|
|
assert(pos.ep_square == 0x24); /* e3 */
|
||
|
|
assert(pos.side == BLACK);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
(void)before;
|
||
|
|
assert(pos.board[0x14] == WP);
|
||
|
|
assert(pos.board[0x34] == EMPTY);
|
||
|
|
assert(pos.side == WHITE);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_capture(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Rxd5: white rook on a5 takes black queen on d5 */
|
||
|
|
parse_fen(&pos, "8/8/8/R2q4/8/8/8/K6k w - - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "a5d5", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_capture);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(cap == BQ);
|
||
|
|
assert(pos.board[0x40] == EMPTY); /* a5 empty */
|
||
|
|
assert(pos.board[0x43] == WR); /* d5 has rook */
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x40] == WR);
|
||
|
|
assert(pos.board[0x43] == BQ);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_en_passant(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black pawn on e4, white just played d2d4, ep on d3 */
|
||
|
|
parse_fen(&pos, "8/8/8/8/3Pp3/8/8/4K2k b - d3 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e4d3", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_enpassant);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
/* White pawn at d4=0x33 should be captured */
|
||
|
|
assert(pos.board[0x33] == EMPTY);
|
||
|
|
/* Black pawn moves from e4=0x34 to d3=0x23 */
|
||
|
|
assert(pos.board[0x34] == EMPTY);
|
||
|
|
assert(pos.board[0x23] == BP);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x33] == WP); /* restored white pawn */
|
||
|
|
assert(pos.board[0x34] == BP); /* black pawn back */
|
||
|
|
assert(pos.board[0x23] == EMPTY);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_white_kingside_castle(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/4K2R w K - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e1g1", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_castle);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x06] == WK);
|
||
|
|
assert(pos.board[0x05] == WR);
|
||
|
|
assert(pos.board[0x07] == EMPTY);
|
||
|
|
assert(!(pos.castle & (1 << 0))); /* white kingside right gone */
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x04] == WK);
|
||
|
|
assert(pos.board[0x07] == WR);
|
||
|
|
assert(pos.board[0x05] == EMPTY);
|
||
|
|
assert(pos.board[0x06] == EMPTY);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_white_queenside_castle(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/R3K3 w Q - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e1c1", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_castle);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x02] == WK);
|
||
|
|
assert(pos.board[0x03] == WR);
|
||
|
|
assert(pos.board[0x00] == EMPTY);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x04] == WK);
|
||
|
|
assert(pos.board[0x00] == WR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_black_kingside_castle(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k2r/8/8/8/8/8/8/4K3 b k - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e8g8", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_castle);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x76] == BK);
|
||
|
|
assert(pos.board[0x75] == BR);
|
||
|
|
assert(pos.board[0x77] == EMPTY);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x74] == BK);
|
||
|
|
assert(pos.board[0x77] == BR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_unmake_black_queenside_castle(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "r3k3/8/8/8/8/8/8/4K3 b q - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e8c8", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.is_castle);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x72] == BK);
|
||
|
|
assert(pos.board[0x73] == BR);
|
||
|
|
assert(pos.board[0x70] == EMPTY);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
assert(pos.board[0x74] == BK);
|
||
|
|
assert(pos.board[0x70] == BR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_promotion_queen(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e7e8q", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.promo == WQ);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x74] == WQ);
|
||
|
|
assert(pos.board[0x64] == EMPTY);
|
||
|
|
|
||
|
|
unmake_move(&pos, &m, cap);
|
||
|
|
/* After unmake: from square should have the pawn (unmake restores moved piece via side) */
|
||
|
|
/* unmake sets side back to white first, then assigns moved piece (WP) from promo branch */
|
||
|
|
assert(pos.board[0x64] == WP);
|
||
|
|
assert(pos.board[0x74] == EMPTY);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_promotion_rook(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e7e8r", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.promo == WR);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x74] == WR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_promotion_bishop(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e7e8b", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.promo == WB);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x74] == WB);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_promotion_knight(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "e7e8n", &m);
|
||
|
|
assert(found);
|
||
|
|
assert(m.promo == WN);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.board[0x74] == WN);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_castling_rights_update_on_rook_move(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1");
|
||
|
|
|
||
|
|
/* Move h1 rook */
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "h1g1", &m);
|
||
|
|
assert(found);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
/* h1 is 0x07; moving from there removes kingside right (bit 0) */
|
||
|
|
assert(!(pos.castle & (1 << 0)));
|
||
|
|
assert(pos.castle & (1 << 1)); /* queenside still intact */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_make_castling_rights_removed_on_a1_capture(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black rook captures white a1 rook */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/R3K3 b Q - 0 1");
|
||
|
|
|
||
|
|
/* Put a black rook on a2 to capture a1 */
|
||
|
|
pos.board[0x10] = BR;
|
||
|
|
Move m;
|
||
|
|
int found = move_from_uci(&pos, "a2a1", &m);
|
||
|
|
assert(found);
|
||
|
|
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(!(pos.castle & (1 << 1))); /* white queenside gone */
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* in_check / gen_moves tests
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_in_check_not_in_check(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
assert(!in_check(&pos, WHITE));
|
||
|
|
assert(!in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_by_rook(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on e1, black rook on e8 - rays clear */
|
||
|
|
parse_fen(&pos, "4r3/8/8/8/8/8/8/4K3 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
assert(!in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_no_king(void)
|
||
|
|
{
|
||
|
|
/* No WK on board - in_check should return 0 gracefully */
|
||
|
|
Position pos;
|
||
|
|
memset(&pos, 0, sizeof(pos));
|
||
|
|
for (int i = 0; i < 128; i++)
|
||
|
|
{
|
||
|
|
pos.board[i] = EMPTY;
|
||
|
|
}
|
||
|
|
pos.ep_square = -1;
|
||
|
|
assert(!in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_by_knight(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on e1 (0x04), black knight on d3 (0x23) attacks e1 */
|
||
|
|
parse_fen(&pos, "8/8/8/8/8/3n4/8/4K3 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_by_bishop(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "8/8/8/8/8/8/6b1/7K w - - 0 1");
|
||
|
|
/* Black bishop on g2, white king on h1 - diagonal */
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_by_pawn(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on e4, black pawn on d5 attacks e4 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/3p4/4K3/8/8/8 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_startpos_count(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
assert(n == 20); /* 16 pawn + 4 knight = 20 */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_captures_only(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 1); /* captures only */
|
||
|
|
assert(n == 0); /* no captures from start */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_only_captures_mid_game(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White rook on d5 can capture black pawn on f5 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/3R1p2/8/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 1);
|
||
|
|
assert(n >= 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_checkmate(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Fool's mate: white checkmated */
|
||
|
|
parse_fen(&pos, "rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
assert(n == 0); /* no legal moves = checkmate */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_stalemate(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Classic stalemate: black king on a8, white queen on c7, white king on c8 */
|
||
|
|
parse_fen(&pos, "k7/2Q5/2K5/8/8/8/8/8 b - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
assert(n == 0);
|
||
|
|
assert(!in_check(&pos, BLACK)); /* stalemate not checkmate */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_promotes_four_choices(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White pawn on e7, kings off the e-file so e8 is clear */
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int from = 0x64; /* e7 */
|
||
|
|
int to = 0x74; /* e8 */
|
||
|
|
int cnt = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == from && moves[i].to == to)
|
||
|
|
{
|
||
|
|
cnt++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(cnt == 4); /* Q, R, B, N promotions */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_en_passant(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/3Pp3/8/8/8/4K3 w - e6 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
/* d5 pawn can capture e.p. to e6=0x54 */
|
||
|
|
int found_ep = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_enpassant)
|
||
|
|
{
|
||
|
|
found_ep = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(found_ep);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_pseudo_not_filtered(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Self-check position: white king on e1, white rook on e2 pinned by black rook on e8.
|
||
|
|
* gen_moves_pseudo should include rook moves; gen_moves should filter them. */
|
||
|
|
parse_fen(&pos, "4r3/8/8/8/8/8/4R3/4K3 w - - 0 1");
|
||
|
|
Move pseudo[256];
|
||
|
|
Move legal[256];
|
||
|
|
int np = gen_moves_pseudo(&pos, pseudo, 256, 0);
|
||
|
|
int nl = gen_moves(&pos, legal, 256, 0);
|
||
|
|
/* Pseudo includes pinned rook moves; legal filters them */
|
||
|
|
assert(np > nl);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_knight_attacks(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Knight on c3, find all squares it attacks */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/2N5/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
/* Knight on c3 (0x22) attacks a2,b1,d1,e2,e4,d5,b5,a4 = 8 squares
|
||
|
|
* Plus king moves */
|
||
|
|
int knight_moves = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x22)
|
||
|
|
{
|
||
|
|
knight_moves++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(knight_moves == 8);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_bishop_attacks(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Bishop on d4, open board */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/3B4/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int bishop_moves = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x33)
|
||
|
|
{
|
||
|
|
bishop_moves++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(bishop_moves == 13); /* d4 bishop has 13 diagonal squares */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_rook_attacks(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/3R4/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int rook_moves = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x33)
|
||
|
|
{
|
||
|
|
rook_moves++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(rook_moves == 14); /* d4 rook: 7 horizontal + 7 vertical = 14 */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_queen_attacks(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/3Q4/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int queen_moves = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x33)
|
||
|
|
{
|
||
|
|
queen_moves++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(queen_moves == 27); /* d4 queen: 13 diagonal + 14 rook = 27 */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_castling_blocked_by_attack(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White kingside castle: f1 attacked by black rook - castle should be illegal */
|
||
|
|
parse_fen(&pos, "4k2r/8/8/8/8/8/8/4K2R w K - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int castle_kg = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_castle && moves[i].to == 0x06)
|
||
|
|
{
|
||
|
|
castle_kg = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/* Black rook on h8 covers g1, but not f1 - castle IS legal.
|
||
|
|
* Let's replace with rook on f8 attacking f1. */
|
||
|
|
(void)n;
|
||
|
|
(void)castle_kg;
|
||
|
|
parse_fen(&pos, "4k1r1/8/8/8/8/8/8/4K2R w K - 0 1");
|
||
|
|
n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
castle_kg = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_castle && moves[i].to == 0x06)
|
||
|
|
{
|
||
|
|
castle_kg = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(!castle_kg); /* castling blocked by attack on f1 */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_castling_blocked_in_check(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king in check, cannot castle */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/4K2R w K - 0 1");
|
||
|
|
/* Put black rook on e8 to give check on e1 */
|
||
|
|
pos.board[0x74] = EMPTY;
|
||
|
|
pos.board[0x74] = BK;
|
||
|
|
pos.board[0x44] = BR; /* e5 */
|
||
|
|
pos.board[0x04] = WK;
|
||
|
|
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int castle_kg = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_castle)
|
||
|
|
{
|
||
|
|
castle_kg = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/* Cannot castle while in check */
|
||
|
|
assert(!castle_kg);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_black_castling(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int qs_castle = 0;
|
||
|
|
int ks_castle = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_castle)
|
||
|
|
{
|
||
|
|
if (moves[i].to == 0x72)
|
||
|
|
{
|
||
|
|
qs_castle = 1;
|
||
|
|
}
|
||
|
|
if (moves[i].to == 0x76)
|
||
|
|
{
|
||
|
|
ks_castle = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(qs_castle);
|
||
|
|
assert(ks_castle);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_black_pawn_attacks(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black pawn on e5, white pawn on d4 and f4 - both capturable */
|
||
|
|
parse_fen(&pos, "4k3/8/8/4p3/3P1P2/8/8/4K3 b - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
/* e5 pawn can go to e4 (push), d4 (capture), f4 (capture) */
|
||
|
|
int from_e5 = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x44)
|
||
|
|
{
|
||
|
|
from_e5++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(from_e5 == 3);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_black_pawn_initial_push(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/4p3/8/8/8/8/8/4K3 b - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
/* e7 pawn: push to e6 and e5 (double push) */
|
||
|
|
int from_e7 = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x64)
|
||
|
|
{
|
||
|
|
from_e7++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(from_e7 == 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_black_promotion(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7K/8/8/8/8/8/4p3/7k b - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int cnt = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x14 && moves[i].to == 0x04)
|
||
|
|
{
|
||
|
|
cnt++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(cnt == 4); /* four promotion choices */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_black_ep(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black pawn on d4, white pawn just moved e2e4, ep square on e3=0x24 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/3pP3/8/8/4K3 b - e3 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int has_ep = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_enpassant)
|
||
|
|
{
|
||
|
|
has_ep = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(has_ep);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* move_from_uci tests
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_move_from_uci_basic(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move m;
|
||
|
|
|
||
|
|
int ok = move_from_uci(&pos, "e2e4", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.from == 0x14);
|
||
|
|
assert(m.to == 0x34);
|
||
|
|
assert(!m.promo);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_invalid_from(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "i9i9", &m);
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_not_in_legal_moves(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move m;
|
||
|
|
/* e2e5 is not a legal move */
|
||
|
|
int ok = move_from_uci(&pos, "e2e5", &m);
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_promotion_queen(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e7e8q", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == WQ);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_promotion_rook(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e7e8r", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == WR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_promotion_bishop(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e7e8b", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == WB);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_promotion_knight(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e7e8n", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == WN);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_promotion_uppercase_r(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e7e8R", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == WR);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_black_promotion(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7K/8/8/8/8/8/4p3/7k b - - 0 1");
|
||
|
|
Move m;
|
||
|
|
int ok = move_from_uci(&pos, "e2e1q", &m);
|
||
|
|
assert(ok);
|
||
|
|
assert(m.promo == BQ);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* Perft correctness tests (verifies move generation end-to-end)
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static unsigned long long perft(Position pos, int depth)
|
||
|
|
{
|
||
|
|
if (depth == 0)
|
||
|
|
{
|
||
|
|
return 1ULL;
|
||
|
|
}
|
||
|
|
Move moves[256];
|
||
|
|
unsigned long long nodes = 0ULL;
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
Position child = pos;
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&child, &moves[i], &cap);
|
||
|
|
nodes += perft(child, depth - 1);
|
||
|
|
}
|
||
|
|
return nodes;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_perft_startpos_depth1(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
assert(perft(pos, 1) == 20ULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_perft_startpos_depth2(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
assert(perft(pos, 2) == 400ULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_perft_ep_position_depth1(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* EP position: black has 22 legal moves at depth 1 */
|
||
|
|
parse_fen(&pos, "rnbqkbnr/pppppppp/8/8/3Pp3/8/PPP1PPPP/RNBQKBNR b KQkq d3 0 1");
|
||
|
|
assert(perft(pos, 1) == 22ULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_perft_kiwipete_depth1(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Custom position with en passant, castling, and complex moves */
|
||
|
|
parse_fen(&pos, "r3k2r/p1ppqpb1/bn2pnp1/2PpP3/1p2P3/2N2N2/PBPP1PPP/R2Q1RK1 w kq - 0 1");
|
||
|
|
assert(perft(pos, 1) == 31ULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_perft_kiwipete_depth2(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "r3k2r/p1ppqpb1/bn2pnp1/2PpP3/1p2P3/2N2N2/PBPP1PPP/R2Q1RK1 w kq - 0 1");
|
||
|
|
assert(perft(pos, 2) == 1315ULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* =========================================================================
|
||
|
|
* Additional coverage tests for specific branches
|
||
|
|
* ========================================================================= */
|
||
|
|
|
||
|
|
static void test_attacked_by_white_king_proximity(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black king on e8 next to white king on e7 - attacked by king */
|
||
|
|
parse_fen(&pos, "4k3/4K3/8/8/8/8/8/8 b - - 0 1");
|
||
|
|
assert(in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_attacked_by_white_pawn(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black king on f6, white pawn on e5 - pawn attacks f6 */
|
||
|
|
parse_fen(&pos, "8/8/5k2/4P3/8/8/8/4K3 b - - 0 1");
|
||
|
|
assert(in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_attacked_by_black_pawn(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on d4, black pawn on e5 - attacks d4? No: e5 pawn attacks d4 and f4 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/4p3/3K4/8/8/8 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_attacked_by_queen_rook_direction(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on a1, black queen on a8 - file attack */
|
||
|
|
parse_fen(&pos, "q7/8/8/8/8/8/8/K6k w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_attacked_slider_blocked(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on a1, black rook on a8, white pawn on a4 blocking */
|
||
|
|
parse_fen(&pos, "r7/8/8/8/P7/8/8/K6k w - - 0 1");
|
||
|
|
assert(!in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_halfmove_clock_pawn_resets(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
pos.halfmove_clock = 10;
|
||
|
|
Move m;
|
||
|
|
move_from_uci(&pos, "e2e4", &m);
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.halfmove_clock == 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_halfmove_clock_piece_increments(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/4K2R w K - 0 1");
|
||
|
|
pos.halfmove_clock = 5;
|
||
|
|
Move m;
|
||
|
|
move_from_uci(&pos, "h1g1", &m);
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.halfmove_clock == 6);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_fullmove_increments_after_black(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
set_startpos(&pos);
|
||
|
|
int prev_full = pos.fullmove_number;
|
||
|
|
/* White move first */
|
||
|
|
Move m;
|
||
|
|
move_from_uci(&pos, "e2e4", &m);
|
||
|
|
Piece cap = EMPTY;
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.fullmove_number == prev_full); /* no increment after white */
|
||
|
|
/* Now black plays */
|
||
|
|
move_from_uci(&pos, "e7e5", &m);
|
||
|
|
make_move(&pos, &m, &cap);
|
||
|
|
assert(pos.fullmove_number == prev_full + 1); /* increment after black */
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_move_from_uci_no_promo_mismatch(void)
|
||
|
|
{
|
||
|
|
/* Move has promotion piece but caller provides no promo char - should fail */
|
||
|
|
Position pos;
|
||
|
|
parse_fen(&pos, "7k/4P3/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move m;
|
||
|
|
/* "e7e8" without promo char - should not match any promotion */
|
||
|
|
int ok = move_from_uci(&pos, "e7e8", &m);
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_white_pawn_blocked(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White pawn on e4, black pawn on e5 - blocked, single push only via e3e4 not possible here;
|
||
|
|
* white pawn on e4 should not be able to push to e5 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/4p3/4P3/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int from = 0x34; /* e4 */
|
||
|
|
int to = 0x44; /* e5 */
|
||
|
|
assert(find_move(moves, n, from, to) == -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test make_piece default branch: unknown char treated as EMPTY (covers lines 43-44) */
|
||
|
|
static void test_make_piece_unknown_char(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* FEN with 'X' as a piece — make_piece returns EMPTY, parse succeeds */
|
||
|
|
int ok = parse_fen(&pos, "X7/8/8/8/8/8/8/k6K w - - 0 1");
|
||
|
|
/* Parse succeeds — 'X' treated as empty square */
|
||
|
|
assert(ok);
|
||
|
|
/* a8 (0x70) should be EMPTY since 'X' → EMPTY */
|
||
|
|
assert(pos.board[0x70] == EMPTY);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_count_moves_to_uses_helper(void)
|
||
|
|
{
|
||
|
|
Move m[3];
|
||
|
|
m[0].to = 0x10;
|
||
|
|
m[1].to = 0x20;
|
||
|
|
m[2].to = 0x10;
|
||
|
|
assert(count_moves_to(m, 3, 0x10) == 2);
|
||
|
|
assert(count_moves_to(m, 3, 0x20) == 1);
|
||
|
|
assert(count_moves_to(m, 3, 0x30) == 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_in_check_by_queen_diagonal(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on e1, black queen on h4 - diagonal attack */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/7q/8/8/4K3 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_white_queenside_castle_no_attack_on_d1(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White can queenside castle - verify no illegal pieces on attack */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/8/R3K3 w Q - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int qs_present = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].is_castle && moves[i].to == 0x02)
|
||
|
|
{
|
||
|
|
qs_present = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(qs_present);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void test_gen_moves_make_captures_only_with_capture(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White knight on c3 can capture a black pawn on b5 */
|
||
|
|
parse_fen(&pos, "4k3/8/8/1p6/8/2N5/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 1); /* captures_only=1 */
|
||
|
|
/* Knight on c3 (0x22) should be able to capture on b5 (0x41) */
|
||
|
|
int found = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x22 && moves[i].is_capture)
|
||
|
|
{
|
||
|
|
found = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(found);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test white knight checking black king (covers line 272: return 1 for WN in square_attacked_by) */
|
||
|
|
static void test_in_check_by_white_knight(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White knight on f3 attacks black king on g5? No, f3+14=g5? 0x25+14=0x33 d4 no.
|
||
|
|
* Use: white knight on d6 (0x53), black king on e8 (0x74).
|
||
|
|
* d6+0x1E? Nope, knight offsets: 33,31,18,14,-33,-31,-18,-14
|
||
|
|
* 0x53 + 0x21 = 0x74 yes! 33 decimal = 0x21. So WN on d6 attacks e8. */
|
||
|
|
parse_fen(&pos, "4k3/8/3N4/8/8/8/8/4K3 b - - 0 1");
|
||
|
|
assert(in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test black king adjacent to white king square (covers line 295 in square_attacked_by for BK) */
|
||
|
|
static void test_square_attacked_by_black_king(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king on d4 (0x33), black king on e5 (0x44) - they are adjacent.
|
||
|
|
* in_check(pos, WHITE) checks if white king is attacked by BLACK.
|
||
|
|
* square_attacked_by(pos, 0x33, BLACK) looks for BK adjacent to 0x33.
|
||
|
|
* e5 = 0x44, 0x44 - 0x33 = 0x11 = 17 which is in kd[] yes. */
|
||
|
|
parse_fen(&pos, "8/8/8/4k3/3K4/8/8/8 w - - 0 1");
|
||
|
|
assert(in_check(&pos, WHITE)); /* black king attacks white king */
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test pawn on s1 diagonal (covers line 305: s1 = sq - 15 = wp on g-file attacking from h-file) */
|
||
|
|
static void test_white_pawn_attacks_s1_diagonal(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black king on h5 (0x47), white pawn on g4 (0x36).
|
||
|
|
* s1 = sq - 15 (decimal) = 0x47 - 0x0F = 0x38 (a5? No: 0x47-0x0F=0x38 which is...
|
||
|
|
* 0x38 = rank 3 (3<<4=0x30), file 8 = off-board (0x08 bit set, 0x38&0x88 = 0x08)
|
||
|
|
* Off board! Let me try black king at f6 (0x55).
|
||
|
|
* s1 = 0x55 - 15 = 0x55 - 0x0F = 0x46 = g5 */
|
||
|
|
/* White pawn on g5 (0x46), black king on f6 (0x55).
|
||
|
|
* Does g5 WP attack f6? WP attacks sq+15 and sq+17 from OWN pawn perspective.
|
||
|
|
* But here square_attacked_by checks pos->board[sq-15] == WP, i.e. pos->board[0x46] == WP. YES!
|
||
|
|
*/
|
||
|
|
parse_fen(&pos, "8/8/5k2/6P1/8/8/8/4K3 b - - 0 1");
|
||
|
|
assert(in_check(&pos, BLACK));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test queen capture via rook direction (covers line 601) */
|
||
|
|
static void test_queen_rook_direction_capture(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White queen on d4, black rook on d7 - queen captures along d-file */
|
||
|
|
parse_fen(&pos, "4k3/3r4/8/8/3Q4/8/8/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int found = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
/* Queen on d4=0x33 capturing rook on d7=0x63 via rook direction */
|
||
|
|
if (moves[i].from == 0x33 && moves[i].to == 0x63 && moves[i].is_capture)
|
||
|
|
{
|
||
|
|
found = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(found);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test king capture (covers line 657) */
|
||
|
|
static void test_king_capture(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White king adjacent to black pawn - king captures it */
|
||
|
|
parse_fen(&pos, "4k3/8/8/8/8/8/5p2/4K3 w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int found = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x04 && moves[i].is_capture)
|
||
|
|
{
|
||
|
|
found = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(found);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test capture promotion (pawn on 7th rank captures diagonally to 8th, covers lines 477-483) */
|
||
|
|
static void test_white_pawn_capture_promotion(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* White pawn on d7, black rook on e8 - captures and promotes */
|
||
|
|
parse_fen(&pos, "4kr2/3P4/8/8/8/8/8/7K w - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int from = 0x63; /* d7 */
|
||
|
|
int to = 0x74; /* e8 */
|
||
|
|
int cnt = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == from && moves[i].to == to && moves[i].is_capture)
|
||
|
|
{
|
||
|
|
cnt++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(cnt == 4); /* Q, R, B, N capture-promotions */
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test black pawn capture promotion (covers black path of lines 477-483) */
|
||
|
|
static void test_black_pawn_capture_promotion(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Black pawn on d2, white rook on e1 - captures and promotes */
|
||
|
|
parse_fen(&pos, "7k/8/8/8/8/8/3p4/4RK2 b - - 0 1");
|
||
|
|
Move moves[256];
|
||
|
|
int n = gen_moves(&pos, moves, 256, 0);
|
||
|
|
int cnt = 0;
|
||
|
|
for (int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
if (moves[i].from == 0x13 && moves[i].to == 0x04 && moves[i].is_capture)
|
||
|
|
{
|
||
|
|
cnt++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert(cnt == 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test add_move overflow (covers line 244: return count when count >= max) */
|
||
|
|
static void test_add_move_max_overflow(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* Use gen_moves with max_moves=0 to trigger count >= max */
|
||
|
|
set_startpos(&pos);
|
||
|
|
Move moves[1];
|
||
|
|
int n = gen_moves(&pos, moves, 0, 0); /* max_moves=0 */
|
||
|
|
assert(n == 0); /* add_move returns count (0) immediately */
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test FEN invalid: no space between side and castling (covers line 153) */
|
||
|
|
static void test_parse_fen_missing_space_after_side(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* "8/8/8/8/8/8/8/8 w- - 0 1": missing space between side 'w' and castling '-' */
|
||
|
|
int ok = parse_fen(&pos, "8/8/8/8/8/8/8/8 w- - 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Test FEN invalid: no space after castling (covers line 191) */
|
||
|
|
static void test_parse_fen_missing_space_after_castling(void)
|
||
|
|
{
|
||
|
|
Position pos;
|
||
|
|
/* After castling "-" expect space, but give no space */
|
||
|
|
int ok = parse_fen(&pos, "8/8/8/8/8/8/8/8 w -- 0 1");
|
||
|
|
assert(!ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
/* parse_fen / set_startpos */
|
||
|
|
test_parse_fen_startpos();
|
||
|
|
test_parse_fen_black_to_move();
|
||
|
|
test_parse_fen_no_castling();
|
||
|
|
test_parse_fen_partial_castling();
|
||
|
|
test_parse_fen_invalid_missing_space();
|
||
|
|
test_parse_fen_invalid_side();
|
||
|
|
test_parse_fen_invalid_ep();
|
||
|
|
test_parse_fen_invalid_castling_char();
|
||
|
|
test_parse_fen_invalid_off_board();
|
||
|
|
test_parse_fen_fullmove_and_halfmove();
|
||
|
|
test_set_startpos();
|
||
|
|
|
||
|
|
/* square_from_algebraic */
|
||
|
|
test_square_from_algebraic_basic();
|
||
|
|
test_square_from_algebraic_promotion();
|
||
|
|
test_square_from_algebraic_null();
|
||
|
|
test_square_from_algebraic_a1();
|
||
|
|
test_square_from_algebraic_h8();
|
||
|
|
|
||
|
|
/* make_move / unmake_move */
|
||
|
|
test_make_unmake_pawn_push();
|
||
|
|
test_make_unmake_capture();
|
||
|
|
test_make_unmake_en_passant();
|
||
|
|
test_make_unmake_white_kingside_castle();
|
||
|
|
test_make_unmake_white_queenside_castle();
|
||
|
|
test_make_unmake_black_kingside_castle();
|
||
|
|
test_make_unmake_black_queenside_castle();
|
||
|
|
test_make_promotion_queen();
|
||
|
|
test_make_promotion_rook();
|
||
|
|
test_make_promotion_bishop();
|
||
|
|
test_make_promotion_knight();
|
||
|
|
test_make_castling_rights_update_on_rook_move();
|
||
|
|
test_make_castling_rights_removed_on_a1_capture();
|
||
|
|
|
||
|
|
/* in_check */
|
||
|
|
test_in_check_not_in_check();
|
||
|
|
test_in_check_by_rook();
|
||
|
|
test_in_check_no_king();
|
||
|
|
test_in_check_by_knight();
|
||
|
|
test_in_check_by_bishop();
|
||
|
|
test_in_check_by_pawn();
|
||
|
|
|
||
|
|
/* gen_moves */
|
||
|
|
test_gen_moves_startpos_count();
|
||
|
|
test_gen_moves_captures_only();
|
||
|
|
test_gen_moves_only_captures_mid_game();
|
||
|
|
test_gen_moves_checkmate();
|
||
|
|
test_gen_moves_stalemate();
|
||
|
|
test_gen_moves_promotes_four_choices();
|
||
|
|
test_gen_moves_en_passant();
|
||
|
|
test_gen_pseudo_not_filtered();
|
||
|
|
test_gen_moves_knight_attacks();
|
||
|
|
test_gen_moves_bishop_attacks();
|
||
|
|
test_gen_moves_rook_attacks();
|
||
|
|
test_gen_moves_queen_attacks();
|
||
|
|
test_gen_moves_castling_blocked_by_attack();
|
||
|
|
test_gen_moves_castling_blocked_in_check();
|
||
|
|
test_gen_moves_black_castling();
|
||
|
|
test_gen_moves_black_pawn_attacks();
|
||
|
|
test_gen_moves_black_pawn_initial_push();
|
||
|
|
test_gen_moves_black_promotion();
|
||
|
|
test_gen_moves_black_ep();
|
||
|
|
|
||
|
|
/* move_from_uci */
|
||
|
|
test_move_from_uci_basic();
|
||
|
|
test_move_from_uci_invalid_from();
|
||
|
|
test_move_from_uci_not_in_legal_moves();
|
||
|
|
test_move_from_uci_promotion_queen();
|
||
|
|
test_move_from_uci_promotion_rook();
|
||
|
|
test_move_from_uci_promotion_bishop();
|
||
|
|
test_move_from_uci_promotion_knight();
|
||
|
|
test_move_from_uci_promotion_uppercase_r();
|
||
|
|
test_move_from_uci_black_promotion();
|
||
|
|
|
||
|
|
/* Perft correctness */
|
||
|
|
test_perft_startpos_depth1();
|
||
|
|
test_perft_startpos_depth2();
|
||
|
|
test_perft_ep_position_depth1();
|
||
|
|
test_perft_kiwipete_depth1();
|
||
|
|
test_perft_kiwipete_depth2();
|
||
|
|
|
||
|
|
/* Additional coverage */
|
||
|
|
test_attacked_by_white_king_proximity();
|
||
|
|
test_attacked_by_white_pawn();
|
||
|
|
test_attacked_by_black_pawn();
|
||
|
|
test_attacked_by_queen_rook_direction();
|
||
|
|
test_attacked_slider_blocked();
|
||
|
|
test_halfmove_clock_pawn_resets();
|
||
|
|
test_halfmove_clock_piece_increments();
|
||
|
|
test_fullmove_increments_after_black();
|
||
|
|
test_move_from_uci_no_promo_mismatch();
|
||
|
|
test_gen_moves_white_pawn_blocked();
|
||
|
|
test_count_moves_to_uses_helper();
|
||
|
|
test_in_check_by_queen_diagonal();
|
||
|
|
test_gen_moves_white_queenside_castle_no_attack_on_d1();
|
||
|
|
test_gen_moves_make_captures_only_with_capture();
|
||
|
|
test_in_check_by_white_knight();
|
||
|
|
test_square_attacked_by_black_king();
|
||
|
|
test_white_pawn_attacks_s1_diagonal();
|
||
|
|
test_queen_rook_direction_capture();
|
||
|
|
test_king_capture();
|
||
|
|
test_white_pawn_capture_promotion();
|
||
|
|
test_black_pawn_capture_promotion();
|
||
|
|
test_add_move_max_overflow();
|
||
|
|
test_parse_fen_missing_space_after_side();
|
||
|
|
test_parse_fen_missing_space_after_castling();
|
||
|
|
test_make_piece_unknown_char();
|
||
|
|
|
||
|
|
printf("All tests passed (%d tests).\n", 89);
|
||
|
|
return 0;
|
||
|
|
}
|