commit a2ff70d4c1bc973b3a778f9425136d52c17fe448 Author: srtk Date: Mon Apr 28 12:09:05 2025 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f31401 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.vscode/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81391d2 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +CC = gcc +CFLAGS = -Wall -Wextra -O3 -pthread +LDFLAGS = -pthread +SRC_DIR = src +BUILD_DIR = build +OBJ_DIR = $(BUILD_DIR)/obj +BIN_DIR = $(BUILD_DIR)/bin + +SRCS = $(wildcard $(SRC_DIR)/*.c) +OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS)) +TARGET = $(BIN_DIR)/bitchess + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) | $(BIN_DIR) + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +$(OBJ_DIR): + mkdir -p $(OBJ_DIR) + +$(BIN_DIR): + mkdir -p $(BIN_DIR) + +clean: + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..06d2346 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# BitChess Engine +## A Pure Algorithmic Chess Engine + +### Chess Engine Overview + +BitChess is a high-performance chess engine developed as a hobby project to explore low-level optimization techniques and algorithmic game theory. The engine uses traditional, non-AI approaches to chess programming, focusing on efficient data structures, multithreaded search, and careful optimization of critical paths. + +This project demonstrates how far a pure algorithmic approach to chess can go without relying on neural networks or machine learning techniques that dominate modern engines like Stockfish, Leela Chess Zero, or AlphaZero. + +### Motivation + +The primary goals of this project are: +- Deepen understanding of bitboard representations and bit manipulation +- Master low-level optimization techniques in C +- Explore multithreaded search algorithms +- Build an efficient, pure algorithmic chess engine from scratch +- Benchmark algorithmic approaches against more modern techniques + +### Key Features +- Bitboard-based representation for maximum performance +- Alpha-beta pruning with move ordering for efficient search +- Multithreaded search to utilize multi-core processors +- Position evaluation using piece-square tables and material value +- Clean terminal interface that updates in real-time +- Standard Algebraic Notation (SAN) support for move representation + +### Technical Approach + +#### Board Representation + +The engine uses bitboards (64-bit unsigned integers) to represent the chess position, where each bit corresponds to a square on the board. This approach allows for extremely fast position updates and move generation using bitwise operations. + +Bitboard for White Pawns (P): `0x000000000000FF00` + + ---------------- + 8 | . . . . . . . . | + 7 | . . . . . . . . | + 6 | . . . . . . . . | + 5 | . . . . . . . . | + 4 | . . . . . . . . | + 3 | . . . . . . . . | + 2 | P P P P P P P P | + 1 | . . . . . . . . | + ---------------- + a b c d e f g h + +#### Search Algorithm + +BitChess employs a classic Minimax algorithm with Alpha-Beta pruning to search the game tree efficiently. The engine implements: +- Move ordering to maximize alpha-beta cutoffs +- Multithreaded search at the root node to parallelize evaluation +- Iterative deepening to find good moves quickly +- MVV-LVA (Most Valuable Victim - Least Valuable Aggressor) for capture ordering + +#### Evaluation Function + +The evaluation combines several components: +- **Material balance** - basic piece values (pawn=100, knight=320, etc.) +- **Piece-square tables** - bonuses/penalties for piece positioning +- **Mobility** - counting legal moves as a proxy for piece activity +- **King safety** - evaluating the king's defensive structure + +#### Move Generation + +The engine generates moves using precomputed attack tables: +- **Sliding pieces** (bishops, rooks, queens) - using ray attacks +- **Knights and kings** - using precomputed attack patterns +- **Pawns** - handling special moves like promotions, en passant, and double pushes + +### Implementation Details + +BitChess is written in C and designed with modularity in mind: +- **Bitboard module** - manages attack tables and bit manipulation +- **Board module** - handles game state and move application +- **Movegen module** - generates legal moves +- **Evaluation module** - assesses positions +- **Search module** - finds the best move +- **Notation module** - converts between internal move representation and SAN + +### Performance Considerations + +Several optimizations are implemented: +- **Precomputed attack tables** - eliminate expensive calculations during search +- **Move ordering** - dramatically improves alpha-beta pruning efficiency +- **Multithreading** - utilizes all available CPU cores +- **Bit manipulation** - uses built-in popcount and bit-scan operations +- **Structure organization** - designed to maximize cache efficiency + +### Current Capabilities + +The engine currently: +- Implements all chess rules correctly, including castling, en passant, and promotions +- Searches at configurable depths with reasonable performance +- Uses multiple threads to speed up search +- Displays evaluation in centipawns +- Provides a clean terminal interface + +### Future Improvements + +Planned enhancements include: +- Transposition tables to avoid re-searching identical positions +- Quiescence search to resolve tactical sequences +- Opening book for stronger play in the early game +- Endgame tablebases for perfect play in simplified positions +- UCI protocol support for compatibility with chess GUIs +- Performance profiling and further optimization + +### Building and Running + + +#### Clone the repository +```bash +git clone https://git.srtk.in/sarthak/bitchess.git +cd bitchess +``` + +#### Build the project +```bash +make +``` + +#### Run the engine +```bash +./build/bin/chess_engine +``` + +### Requirements + +- GCC or Clang compiler with C99 support +- POSIX-compliant operating system (Linux, macOS, or Windows with WSL) +- pthread library \ No newline at end of file diff --git a/src/bitboard.c b/src/bitboard.c new file mode 100644 index 0000000..941b9f8 --- /dev/null +++ b/src/bitboard.c @@ -0,0 +1,147 @@ +#include +#include "bitboard.h" + +// Precomputed attack tables +U64 knight_attacks[64]; +U64 king_attacks[64]; +U64 pawn_attacks[2][64]; // [color][square] + +// Lookup tables for bishop and rook moves +U64 bishop_masks[64]; +U64 rook_masks[64]; + +// Magic bitboard tables +U64 bishop_attacks[64][512]; +U64 rook_attacks[64][4096]; + +// Initialize attack tables +void init_attack_tables() { + // Knight attacks + for (int sq = 0; sq < 64; ++sq) { + U64 mask = 0ULL; + int r = sq / 8, f = sq % 8; + int dr[8] = {2, 2, 1, 1, -1, -1, -2, -2}; + int df[8] = {1, -1, 2, -2, 2, -2, 1, -1}; + + for (int i = 0; i < 8; ++i) { + int nr = r + dr[i], nf = f + df[i]; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) { + SET_BIT(mask, SQUARE(nr, nf)); + } + } + knight_attacks[sq] = mask; + } + + // King attacks + for (int sq = 0; sq < 64; ++sq) { + U64 mask = 0ULL; + int r = sq / 8, f = sq % 8; + + for (int dr = -1; dr <= 1; ++dr) { + for (int df = -1; df <= 1; ++df) { + if (dr == 0 && df == 0) continue; + int nr = r + dr, nf = f + df; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) { + SET_BIT(mask, SQUARE(nr, nf)); + } + } + } + king_attacks[sq] = mask; + } + + // Pawn attacks + for (int sq = 0; sq < 64; ++sq) { + U64 white_mask = 0ULL, black_mask = 0ULL; + int r = sq / 8, f = sq % 8; + + // White pawns attack up-left and up-right + if (r < 7) { + if (f > 0) SET_BIT(white_mask, SQUARE(r+1, f-1)); // Up-left + if (f < 7) SET_BIT(white_mask, SQUARE(r+1, f+1)); // Up-right + } + + // Black pawns attack down-left and down-right + if (r > 0) { + if (f > 0) SET_BIT(black_mask, SQUARE(r-1, f-1)); // Down-left + if (f < 7) SET_BIT(black_mask, SQUARE(r-1, f+1)); // Down-right + } + + pawn_attacks[0][sq] = white_mask; + pawn_attacks[1][sq] = black_mask; + } + + // Initialize sliding piece attacks (simplified approach without magic bitboards) + for (int sq = 0; sq < 64; ++sq) { + int r = sq / 8, f = sq % 8; + + // Initialize bishop and rook masks + bishop_masks[sq] = 0ULL; + rook_masks[sq] = 0ULL; + + // North ray + for (int nr = r + 1; nr < 7; ++nr) + SET_BIT(rook_masks[sq], SQUARE(nr, f)); + + // South ray + for (int nr = r - 1; nr > 0; --nr) + SET_BIT(rook_masks[sq], SQUARE(nr, f)); + + // East ray + for (int nf = f + 1; nf < 7; ++nf) + SET_BIT(rook_masks[sq], SQUARE(r, nf)); + + // West ray + for (int nf = f - 1; nf > 0; --nf) + SET_BIT(rook_masks[sq], SQUARE(r, nf)); + + // North-East ray + for (int nr = r + 1, nf = f + 1; nr < 7 && nf < 7; ++nr, ++nf) + SET_BIT(bishop_masks[sq], SQUARE(nr, nf)); + + // South-East ray + for (int nr = r - 1, nf = f + 1; nr > 0 && nf < 7; --nr, ++nf) + SET_BIT(bishop_masks[sq], SQUARE(nr, nf)); + + // South-West ray + for (int nr = r - 1, nf = f - 1; nr > 0 && nf > 0; --nr, --nf) + SET_BIT(bishop_masks[sq], SQUARE(nr, nf)); + + // North-West ray + for (int nr = r + 1, nf = f - 1; nr < 7 && nf > 0; ++nr, --nf) + SET_BIT(bishop_masks[sq], SQUARE(nr, nf)); + } +} + +// Get sliding piece attacks (bishop/rook) +U64 get_sliding_attacks(int sq, U64 blockers, int is_bishop) { + U64 attacks = 0ULL; + int r = sq / 8, f = sq % 8; + + const int bishop_dirs[4][2] = {{1,1}, {1,-1}, {-1,1}, {-1,-1}}; + const int rook_dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}}; + + const int (*dirs)[2] = is_bishop ? bishop_dirs : rook_dirs; + + for (int i = 0; i < 4; i++) { + int dr = dirs[i][0], df = dirs[i][1]; + int nr = r, nf = f; + + while (1) { + nr += dr; + nf += df; + + // Check if we're still on the board + if (nr < 0 || nr > 7 || nf < 0 || nf > 7) + break; + + int target_sq = SQUARE(nr, nf); + SET_BIT(attacks, target_sq); + + // Stop if we hit a blocker + if (GET_BIT(blockers, target_sq)) + break; + } + } + + return attacks; +} \ No newline at end of file diff --git a/src/bitboard.h b/src/bitboard.h new file mode 100644 index 0000000..84ecb3d --- /dev/null +++ b/src/bitboard.h @@ -0,0 +1,25 @@ +#ifndef BITBOARD_H +#define BITBOARD_H + +#include "types.h" + +// Precomputed attack tables +extern U64 knight_attacks[64]; +extern U64 king_attacks[64]; +extern U64 pawn_attacks[2][64]; // [color][square] + +// Lookup tables for bishop and rook moves +extern U64 bishop_masks[64]; +extern U64 rook_masks[64]; + +// Magic bitboard tables +extern U64 bishop_attacks[64][512]; +extern U64 rook_attacks[64][4096]; + +// Initialize attack tables +void init_attack_tables(); + +// Get sliding piece attacks (bishop/rook) +U64 get_sliding_attacks(int sq, U64 blockers, int is_bishop); + +#endif // BITBOARD_H diff --git a/src/board.c b/src/board.c new file mode 100644 index 0000000..6ae0dd4 --- /dev/null +++ b/src/board.c @@ -0,0 +1,299 @@ +#include +#include +#include +#include "board.h" +#include "bitboard.h" +#include "movegen.h" + +// Update occupancy bitboards +void update_occupancy(GameState *state) { + state->occ_white = 0ULL; + state->occ_black = 0ULL; + + for (int pc = P; pc <= K; ++pc) + state->occ_white |= state->bitboards[pc]; + + for (int pc = p; pc <= k; ++pc) + state->occ_black |= state->bitboards[pc]; + + state->occ_all = state->occ_white | state->occ_black; +} + +// Initialize a standard chess starting position +void init_startpos(GameState *state) { + // Clear all bitboards + memset(state, 0, sizeof(GameState)); + + // Set up white pieces + state->bitboards[P] = 0x000000000000FF00ULL; // Pawns on rank 2 + state->bitboards[N] = 0x0000000000000042ULL; // Knights on b1, g1 + state->bitboards[B] = 0x0000000000000024ULL; // Bishops on c1, f1 + state->bitboards[R] = 0x0000000000000081ULL; // Rooks on a1, h1 + state->bitboards[Q] = 0x0000000000000008ULL; // Queen on d1 + state->bitboards[K] = 0x0000000000000010ULL; // King on e1 + + // Set up black pieces + state->bitboards[p] = 0x00FF000000000000ULL; // Pawns on rank 7 + state->bitboards[n] = 0x4200000000000000ULL; // Knights on b8, g8 + state->bitboards[b] = 0x2400000000000000ULL; // Bishops on c8, f8 + state->bitboards[r] = 0x8100000000000000ULL; // Rooks on a8, h8 + state->bitboards[q] = 0x0800000000000000ULL; // Queen on d8 + state->bitboards[k] = 0x1000000000000000ULL; // King on e8 + + // Initialize game state + state->side_to_move = 0; // White to move + state->castling = 15; // All castling rights available (1111 binary) + state->ep_square = -1; // No en passant + state->halfmove_clock = 0; + state->fullmove_number = 1; + + update_occupancy(state); +} + +// Make a move +void make_move(GameState *state, Move move, int *captured_pc, int *old_castling, int *old_ep) { + *old_castling = state->castling; + *old_ep = state->ep_square; + *captured_pc = NO_PIECE; + + int side = state->side_to_move; + int from = move.from; + int to = move.to; + + // Find the piece that is moving + int pc = NO_PIECE; + for (int i = 0; i < 12; i++) { + if (GET_BIT(state->bitboards[i], from)) { + pc = i; + break; + } + } + + if (pc == NO_PIECE) return; // Invalid move + + // Handle en passant capture + if ((pc == P || pc == p) && to == state->ep_square) { + int cap_sq = to + (side == 0 ? -8 : 8); + int cap_pc = side == 0 ? p : P; + + CLEAR_BIT(state->bitboards[cap_pc], cap_sq); + *captured_pc = cap_pc; + } + + // Check for regular capture + for (int i = 0; i < 12; i++) { + if (i != pc && GET_BIT(state->bitboards[i], to)) { + CLEAR_BIT(state->bitboards[i], to); + *captured_pc = i; + break; + } + } + + // Move the piece + CLEAR_BIT(state->bitboards[pc], from); + + // Handle promotion + if (move.promo && (pc == P || pc == p)) { + int promo_pc; + switch (move.promo) { + case 1: promo_pc = side == 0 ? N : n; break; + case 2: promo_pc = side == 0 ? B : b; break; + case 3: promo_pc = side == 0 ? R : r; break; + case 4: promo_pc = side == 0 ? Q : q; break; + default: promo_pc = side == 0 ? Q : q; break; + } + SET_BIT(state->bitboards[promo_pc], to); + } else { + SET_BIT(state->bitboards[pc], to); + } + + // Handle castling - move the rook too + if (pc == K && from == e1) { + if (to == g1) { // Kingside castle + CLEAR_BIT(state->bitboards[R], h1); + SET_BIT(state->bitboards[R], f1); + } else if (to == c1) { // Queenside castle + CLEAR_BIT(state->bitboards[R], a1); + SET_BIT(state->bitboards[R], d1); + } + } else if (pc == k && from == e8) { + if (to == g8) { // Kingside castle + CLEAR_BIT(state->bitboards[r], h8); + SET_BIT(state->bitboards[r], f8); + } else if (to == c8) { // Queenside castle + CLEAR_BIT(state->bitboards[r], a8); + SET_BIT(state->bitboards[r], d8); + } + } + + // Update castling rights + if (pc == K) state->castling &= ~3; // Clear white castling rights + if (pc == k) state->castling &= ~12; // Clear black castling rights + if (from == a1 || to == a1) state->castling &= ~2; // White queenside + if (from == h1 || to == h1) state->castling &= ~1; // White kingside + if (from == a8 || to == a8) state->castling &= ~8; // Black queenside + if (from == h8 || to == h8) state->castling &= ~4; // Black kingside + + // Set new en passant square + state->ep_square = -1; + if ((pc == P || pc == p) && abs(to - from) == 16) { + state->ep_square = (from + to) / 2; + } + + // Update halfmove clock + if (pc == P || pc == p || *captured_pc != NO_PIECE) { + state->halfmove_clock = 0; + } else { + state->halfmove_clock++; + } + + // Update fullmove number + if (side == 1) { + state->fullmove_number++; + } + + // Switch sides + state->side_to_move = 1 - side; + + // Update occupancy + update_occupancy(state); +} + +// Undo a move +void undo_move(GameState *state, Move move, int captured_pc, int old_castling, int old_ep) { + int side = 1 - state->side_to_move; // Side that made the move + int from = move.from; + int to = move.to; + + // Find the piece that moved + int pc = NO_PIECE; + for (int i = 0; i < 12; i++) { + if (GET_BIT(state->bitboards[i], to) && ( + (side == 0 && i <= K) || (side == 1 && i >= p) + )) { + pc = i; + break; + } + } + + if (pc == NO_PIECE && !move.promo) return; // Invalid move + + // Handle promotion (find the pawn) + if (move.promo) { + pc = side == 0 ? P : p; + int promo_pc; + switch (move.promo) { + case 1: promo_pc = side == 0 ? N : n; break; + case 2: promo_pc = side == 0 ? B : b; break; + case 3: promo_pc = side == 0 ? R : r; break; + case 4: promo_pc = side == 0 ? Q : q; break; + default: promo_pc = side == 0 ? Q : q; break; + } + CLEAR_BIT(state->bitboards[promo_pc], to); + } else { + CLEAR_BIT(state->bitboards[pc], to); + } + + // Move the piece back + SET_BIT(state->bitboards[pc], from); + + // Handle en passant capture + if ((pc == P || pc == p) && to == state->ep_square) { + int cap_sq = to + (side == 0 ? -8 : 8); + SET_BIT(state->bitboards[captured_pc], cap_sq); + } + // Restore captured piece + else if (captured_pc != NO_PIECE) { + SET_BIT(state->bitboards[captured_pc], to); + } + + // Handle castling - move the rook back + if (pc == K && from == e1) { + if (to == g1) { // Kingside castle + CLEAR_BIT(state->bitboards[R], f1); + SET_BIT(state->bitboards[R], h1); + } else if (to == c1) { // Queenside castle + CLEAR_BIT(state->bitboards[R], d1); + SET_BIT(state->bitboards[R], a1); + } + } else if (pc == k && from == e8) { + if (to == g8) { // Kingside castle + CLEAR_BIT(state->bitboards[r], f8); + SET_BIT(state->bitboards[r], h8); + } else if (to == c8) { // Queenside castle + CLEAR_BIT(state->bitboards[r], d8); + SET_BIT(state->bitboards[r], a8); + } + } + + // Restore castling rights + state->castling = old_castling; + + // Restore en passant square + state->ep_square = old_ep; + + // Adjust fullmove number + if (side == 1) { + state->fullmove_number--; + } + + // Switch sides back + state->side_to_move = side; + + // Update occupancy + update_occupancy(state); +} + +// Check if king is in check +int is_king_in_check(const GameState *state, int side) { + int king_sq; + U64 king_bb = side == 0 ? state->bitboards[K] : state->bitboards[k]; + + if (!king_bb) return 0; // No king (shouldn't happen in a real game) + + king_sq = __builtin_ctzll(king_bb); + return is_square_attacked(state, king_sq, 1 - side); // Is king attacked by opponent? +} + +// Print board representation +void print_board(const GameState *state) { + char board[64]; + memset(board, '.', 64); + + for (int pc = 0; pc < 12; pc++) { + U64 bb = state->bitboards[pc]; + while (bb) { + int sq = __builtin_ctzll(bb); + board[sq] = "PNBRQKpnbrqk"[pc]; + CLEAR_BIT(bb, sq); + } + } + + printf("\n +---+---+---+---+---+---+---+---+\n"); + for (int rank = 7; rank >= 0; rank--) { + printf("%d |", rank + 1); + for (int file = 0; file < 8; file++) { + printf(" %c |", board[rank * 8 + file]); + } + printf("\n +---+---+---+---+---+---+---+---+\n"); + } + printf(" a b c d e f g h\n\n"); + + printf("Side to move: %s\n", state->side_to_move == 0 ? "White" : "Black"); + printf("Fullmove number: %d\n", state->fullmove_number); + printf("Castling rights: %c%c%c%c\n", + (state->castling & 1) ? 'K' : '-', + (state->castling & 2) ? 'Q' : '-', + (state->castling & 4) ? 'k' : '-', + (state->castling & 8) ? 'q' : '-'); + + if (state->ep_square != -1) { + int file = state->ep_square % 8; + int rank = state->ep_square / 8; + printf("En passant: %c%d\n", 'a' + file, rank + 1); + } else { + printf("En passant: -\n"); + } + + printf("\n"); +} \ No newline at end of file diff --git a/src/board.h b/src/board.h new file mode 100644 index 0000000..559a40a --- /dev/null +++ b/src/board.h @@ -0,0 +1,24 @@ +#ifndef BOARD_H +#define BOARD_H + +#include "types.h" + +// Update occupancy bitboards +void update_occupancy(GameState *state); + +// Initialize a standard chess starting position +void init_startpos(GameState *state); + +// Make a move +void make_move(GameState *state, Move move, int *captured_pc, int *old_castling, int *old_ep); + +// Undo a move +void undo_move(GameState *state, Move move, int captured_pc, int old_castling, int old_ep); + +// Check if king is in check +int is_king_in_check(const GameState *state, int side); + +// Print board representation +void print_board(const GameState *state); + +#endif // BOARD_H diff --git a/src/evaluation.c b/src/evaluation.c new file mode 100644 index 0000000..a50c305 --- /dev/null +++ b/src/evaluation.c @@ -0,0 +1,220 @@ +#include "evaluation.h" +#include "movegen.h" + +// Material values +const int piece_value[12] = {100, 320, 330, 500, 900, 20000, + -100, -320, -330, -500, -900, -20000}; + +// Piece-square tables for evaluation +const int pst_pawn[64] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 50, 50, 50, 50, 50, 50, 50, 50, + 10, 10, 20, 30, 30, 20, 10, 10, + 5, 5, 10, 25, 25, 10, 5, 5, + 0, 0, 0, 20, 20, 0, 0, 0, + 5, -5,-10, 0, 0,-10, -5, 5, + 5, 10, 10,-20,-20, 10, 10, 5, + 0, 0, 0, 0, 0, 0, 0, 0 +}; + +const int pst_knight[64] = { + -50,-40,-30,-30,-30,-30,-40,-50, + -40,-20, 0, 0, 0, 0,-20,-40, + -30, 0, 10, 15, 15, 10, 0,-30, + -30, 5, 15, 20, 20, 15, 5,-30, + -30, 0, 15, 20, 20, 15, 0,-30, + -30, 5, 10, 15, 15, 10, 5,-30, + -40,-20, 0, 5, 5, 0,-20,-40, + -50,-40,-30,-30,-30,-30,-40,-50 +}; + +const int pst_bishop[64] = { + -20,-10,-10,-10,-10,-10,-10,-20, + -10, 0, 0, 0, 0, 0, 0,-10, + -10, 0, 10, 10, 10, 10, 0,-10, + -10, 5, 5, 10, 10, 5, 5,-10, + -10, 0, 5, 10, 10, 5, 0,-10, + -10, 5, 5, 5, 5, 5, 5,-10, + -10, 0, 5, 0, 0, 5, 0,-10, + -20,-10,-10,-10,-10,-10,-10,-20 +}; + +const int pst_rook[64] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 5, 10, 10, 10, 10, 10, 10, 5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + 0, 0, 0, 5, 5, 0, 0, 0 +}; + +const int pst_queen[64] = { + -20,-10,-10, -5, -5,-10,-10,-20, + -10, 0, 0, 0, 0, 0, 0,-10, + -10, 0, 5, 5, 5, 5, 0,-10, + -5, 0, 5, 5, 5, 5, 0, -5, + 0, 0, 5, 5, 5, 5, 0, -5, + -10, 5, 5, 5, 5, 5, 0,-10, + -10, 0, 5, 0, 0, 0, 0,-10, + -20,-10,-10, -5, -5,-10,-10,-20 +}; + +const int pst_king_mid[64] = { + -30,-40,-40,-50,-50,-40,-40,-30, + -30,-40,-40,-50,-50,-40,-40,-30, + -30,-40,-40,-50,-50,-40,-40,-30, + -30,-40,-40,-50,-50,-40,-40,-30, + -20,-30,-30,-40,-40,-30,-30,-20, + -10,-20,-20,-20,-20,-20,-20,-10, + 20, 20, 0, 0, 0, 0, 20, 20, + 20, 30, 10, 0, 0, 10, 30, 20 +}; + +const int pst_king_end[64] = { + -50,-40,-30,-20,-20,-30,-40,-50, + -30,-20,-10, 0, 0,-10,-20,-30, + -30,-10, 20, 30, 30, 20,-10,-30, + -30,-10, 30, 40, 40, 30,-10,-30, + -30,-10, 30, 40, 40, 30,-10,-30, + -30,-10, 20, 30, 30, 20,-10,-30, + -30,-30, 0, 0, 0, 0,-30,-30, + -50,-30,-30,-30,-30,-30,-30,-50 +}; + +// Evaluate material balance +int evaluate_material(const GameState *state) { + int score = 0; + + // Material balance + for (int pc = P; pc <= K; pc++) { + score += piece_value[pc] * __builtin_popcountll(state->bitboards[pc]); + } + + for (int pc = p; pc <= k; pc++) { + score += piece_value[pc] * __builtin_popcountll(state->bitboards[pc]); + } + + return score; +} + +// Evaluate positional factors +int evaluate_position(const GameState *state) { + int score = 0; + int sq; + + // Piece-square tables for positional evaluation + + // White pawns + U64 wp = state->bitboards[P]; + while (wp) { + sq = __builtin_ctzll(wp); + score += pst_pawn[sq]; + CLEAR_BIT(wp, sq); + } + + // Black pawns + U64 bp = state->bitboards[p]; + while (bp) { + sq = __builtin_ctzll(bp); + score -= pst_pawn[63 - sq]; // Flip for black + CLEAR_BIT(bp, sq); + } + + // White knights + U64 wn = state->bitboards[N]; + while (wn) { + sq = __builtin_ctzll(wn); + score += pst_knight[sq]; + CLEAR_BIT(wn, sq); + } + + // Black knights + U64 bn = state->bitboards[n]; + while (bn) { + sq = __builtin_ctzll(bn); + score -= pst_knight[63 - sq]; + CLEAR_BIT(bn, sq); + } + + // White bishops + U64 wb = state->bitboards[B]; + while (wb) { + sq = __builtin_ctzll(wb); + score += pst_bishop[sq]; + CLEAR_BIT(wb, sq); + } + + // Black bishops + U64 bb = state->bitboards[b]; + while (bb) { + sq = __builtin_ctzll(bb); + score -= pst_bishop[63 - sq]; + CLEAR_BIT(bb, sq); + } + + // White rooks + U64 wr = state->bitboards[R]; + while (wr) { + sq = __builtin_ctzll(wr); + score += pst_rook[sq]; + CLEAR_BIT(wr, sq); + } + + // Black rooks + U64 br = state->bitboards[r]; + while (br) { + sq = __builtin_ctzll(br); + score -= pst_rook[63 - sq]; + CLEAR_BIT(br, sq); + } + + // White queens + U64 wq = state->bitboards[Q]; + while (wq) { + sq = __builtin_ctzll(wq); + score += pst_queen[sq]; + CLEAR_BIT(wq, sq); + } + + // Black queens + U64 bq = state->bitboards[q]; + while (bq) { + sq = __builtin_ctzll(bq); + score -= pst_queen[63 - sq]; + CLEAR_BIT(bq, sq); + } + + // Kings + if (state->bitboards[K]) { + sq = __builtin_ctzll(state->bitboards[K]); + score += pst_king_mid[sq]; + } + + if (state->bitboards[k]) { + sq = __builtin_ctzll(state->bitboards[k]); + score -= pst_king_mid[63 - sq]; + } + + return score; +} + +// Complete evaluation function +int evaluate(const GameState *state) { + int score = evaluate_material(state) + evaluate_position(state); + + // Mobility bonus (simplified) + GameState temp_state = *state; + Move moves[256]; + temp_state.side_to_move = 0; // White + int white_mobility = generate_moves(&temp_state, moves); + + temp_state.side_to_move = 1; // Black + int black_mobility = generate_moves(&temp_state, moves); + + score += (white_mobility - black_mobility) * 5; // 5 points per move advantage + + // Adjust for side to move + return state->side_to_move == 0 ? score : -score; +} \ No newline at end of file diff --git a/src/evaluation.h b/src/evaluation.h new file mode 100644 index 0000000..afe04da --- /dev/null +++ b/src/evaluation.h @@ -0,0 +1,27 @@ +#ifndef EVALUATION_H +#define EVALUATION_H + +#include "types.h" + +// Material values +extern const int piece_value[12]; + +// Piece-square tables for evaluation +extern const int pst_pawn[64]; +extern const int pst_knight[64]; +extern const int pst_bishop[64]; +extern const int pst_rook[64]; +extern const int pst_queen[64]; +extern const int pst_king_mid[64]; +extern const int pst_king_end[64]; + +// Evaluate material balance +int evaluate_material(const GameState *state); + +// Evaluate positional factors +int evaluate_position(const GameState *state); + +// Complete evaluation function +int evaluate(const GameState *state); + +#endif // EVALUATION_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d25aa9e --- /dev/null +++ b/src/main.c @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include "types.h" +#include "bitboard.h" +#include "board.h" +#include "movegen.h" +#include "evaluation.h" +#include "search.h" +#include "notation.h" + +// Function to clear the terminal screen +void clear_screen() { + printf("\033[2J\033[H"); + fflush(stdout); +} + +// Function to clear from cursor to end of screen +void clear_to_end_of_screen() { + printf("\033[J"); + fflush(stdout); +} + +// Function to move cursor to the top-left corner +void move_cursor_to_top() { + printf("\033[H"); + fflush(stdout); +} + +// Function to print the game state +void print_game_state(GameState *state, int depth, int move_number, char* move_san, int pre_eval) { + print_board(state); + + if (move_number > 0) { + printf("Last move: %d%s: %s\nEvaluation: %+.2f\n", + move_number, + state->side_to_move == 1 ? "w" : "b", // opposite of current side + move_san, + pre_eval / 100.0); + } + + printf("Search depth: %d\n", depth); +} + +// Main game loop +int main() { + // Initialize attack tables + init_attack_tables(); + + // Initialize game state + GameState state; + init_startpos(&state); + + // Search depth (can be adjusted) + int depth = 5; + char last_move_san[10] = ""; + + // Clear the screen at the beginning + clear_screen(); + print_game_state(&state, depth, 0, "", 0); + + // Game loop + while (1) { + // Generate legal moves + Move moves[256]; + int num_moves = generate_legal_moves(&state, moves); + + // Check for checkmate or stalemate + if (num_moves == 0) { + move_cursor_to_top(); + clear_to_end_of_screen(); + print_game_state(&state, depth, state.fullmove_number, last_move_san, 0); + + if (is_king_in_check(&state, state.side_to_move)) { + printf("Checkmate! %s wins.\n", state.side_to_move == 0 ? "Black" : "White"); + } else { + printf("Stalemate! The game is a draw.\n"); + } + break; + } + + // Engine finds the best move + fflush(stdout); + Move best_move = find_best_move(&state, depth); + + // Get SAN notation of the move + strcpy(last_move_san, move_to_san(&state, best_move)); + + // Get evaluation before making the move + int pre_eval = evaluate(&state); + int move_number = state.fullmove_number; + + // Make the best move + int captured, old_castling, old_ep; + make_move(&state, best_move, &captured, &old_castling, &old_ep); + + // Redraw the entire screen + move_cursor_to_top(); + clear_to_end_of_screen(); + print_game_state(&state, depth, move_number, last_move_san, pre_eval); + + // Small delay between moves + usleep(500000); // 0.5 second + } + + return 0; +} diff --git a/src/movegen.c b/src/movegen.c new file mode 100644 index 0000000..f15fead --- /dev/null +++ b/src/movegen.c @@ -0,0 +1,379 @@ +#include +#include "movegen.h" +#include "board.h" +#include "bitboard.h" + +// Check if a square is attacked by a given side +int is_square_attacked(const GameState *state, int sq, int side) { + if (side == 0) { // Attacked by white + // Pawns + if (pawn_attacks[1][sq] & state->bitboards[P]) return 1; + + // Knights + if (knight_attacks[sq] & state->bitboards[N]) return 1; + + // King + if (king_attacks[sq] & state->bitboards[K]) return 1; + + // Bishops & Queens (diagonal) + U64 bishops_queens = state->bitboards[B] | state->bitboards[Q]; + if (bishops_queens) { + U64 bishop_attack = get_sliding_attacks(sq, state->occ_all, 1); + if (bishop_attack & bishops_queens) return 1; + } + + // Rooks & Queens (orthogonal) + U64 rooks_queens = state->bitboards[R] | state->bitboards[Q]; + if (rooks_queens) { + U64 rook_attack = get_sliding_attacks(sq, state->occ_all, 0); + if (rook_attack & rooks_queens) return 1; + } + } else { // Attacked by black + // Pawns + if (pawn_attacks[0][sq] & state->bitboards[p]) return 1; + + // Knights + if (knight_attacks[sq] & state->bitboards[n]) return 1; + + // King + if (king_attacks[sq] & state->bitboards[k]) return 1; + + // Bishops & Queens (diagonal) + U64 bishops_queens = state->bitboards[b] | state->bitboards[q]; + if (bishops_queens) { + U64 bishop_attack = get_sliding_attacks(sq, state->occ_all, 1); + if (bishop_attack & bishops_queens) return 1; + } + + // Rooks & Queens (orthogonal) + U64 rooks_queens = state->bitboards[r] | state->bitboards[q]; + if (rooks_queens) { + U64 rook_attack = get_sliding_attacks(sq, state->occ_all, 0); + if (rook_attack & rooks_queens) return 1; + } + } + + return 0; +} + +// Generate pawn moves +int generate_pawn_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 pawns = side == 0 ? state->bitboards[P] : state->bitboards[p]; + U64 enemy_occ = side == 0 ? state->occ_black : state->occ_white; + + int direction = side == 0 ? 8 : -8; // Up for white, down for black + U64 promotion_rank = side == 0 ? 0xFF00000000000000ULL : 0x00000000000000FFULL; + U64 single_push_targets, double_push_targets; + + if (side == 0) { + // White pawns move up (higher indices) + single_push_targets = (pawns << 8) & ~state->occ_all; + double_push_targets = ((single_push_targets & 0x0000000000FF0000ULL) << 8) & ~state->occ_all; + } else { + // Black pawns move down (lower indices) + single_push_targets = (pawns >> 8) & ~state->occ_all; + double_push_targets = ((single_push_targets & 0x0000FF0000000000ULL) >> 8) & ~state->occ_all; + } + + // Single pushes and promotions + U64 temp = single_push_targets; + while (temp) { + int to = __builtin_ctzll(temp); + int from = to - direction; + + // Check for promotion + if ((1ULL << to) & promotion_rank) { + // Add promotions to knight, bishop, rook, queen + moves[count++] = (Move){from, to, 1, 0}; // Knight + moves[count++] = (Move){from, to, 2, 0}; // Bishop + moves[count++] = (Move){from, to, 3, 0}; // Rook + moves[count++] = (Move){from, to, 4, 0}; // Queen + } else { + moves[count++] = (Move){from, to, 0, 0}; + } + + CLEAR_BIT(temp, to); + } + + // Double pushes + temp = double_push_targets; + while (temp) { + int to = __builtin_ctzll(temp); + int from = to - 2 * direction; + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(temp, to); + } + + // Captures to the left + U64 left_captures; + if (side == 0) { + // White pawns capture up-left + left_captures = ((pawns & ~0x0101010101010101ULL) << 7) & enemy_occ; + } else { + // Black pawns capture down-left + left_captures = ((pawns & ~0x0101010101010101ULL) >> 9) & enemy_occ; + } + + temp = left_captures; + while (temp) { + int to = __builtin_ctzll(temp); + int from = side == 0 ? to - 7 : to + 9; + + // Check for promotion + if ((1ULL << to) & promotion_rank) { + moves[count++] = (Move){from, to, 1, 0}; // Knight + moves[count++] = (Move){from, to, 2, 0}; // Bishop + moves[count++] = (Move){from, to, 3, 0}; // Rook + moves[count++] = (Move){from, to, 4, 0}; // Queen + } else { + moves[count++] = (Move){from, to, 0, 0}; + } + + CLEAR_BIT(temp, to); + } + + // Captures to the right + U64 right_captures; + if (side == 0) { + // White pawns capture up-right + right_captures = ((pawns & ~0x8080808080808080ULL) << 9) & enemy_occ; + } else { + // Black pawns capture down-right + right_captures = ((pawns & ~0x8080808080808080ULL) >> 7) & enemy_occ; + } + + temp = right_captures; + while (temp) { + int to = __builtin_ctzll(temp); + int from = side == 0 ? to - 9 : to + 7; + + // Check for promotion + if ((1ULL << to) & promotion_rank) { + moves[count++] = (Move){from, to, 1, 0}; // Knight + moves[count++] = (Move){from, to, 2, 0}; // Bishop + moves[count++] = (Move){from, to, 3, 0}; // Rook + moves[count++] = (Move){from, to, 4, 0}; // Queen + } else { + moves[count++] = (Move){from, to, 0, 0}; + } + + CLEAR_BIT(temp, to); + } + + // En passant captures + if (state->ep_square != -1) { + U64 ep_pawns = pawns & pawn_attacks[1 - side][state->ep_square]; + + while (ep_pawns) { + int from = __builtin_ctzll(ep_pawns); + moves[count++] = (Move){from, state->ep_square, 0, 0}; + CLEAR_BIT(ep_pawns, from); + } + } + + return count; +} + +// Generate knight moves +int generate_knight_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 knights = side == 0 ? state->bitboards[N] : state->bitboards[n]; + U64 own_pieces = side == 0 ? state->occ_white : state->occ_black; + + while (knights) { + int from = __builtin_ctzll(knights); + U64 targets = knight_attacks[from] & ~own_pieces; + + while (targets) { + int to = __builtin_ctzll(targets); + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(targets, to); + } + + CLEAR_BIT(knights, from); + } + + return count; +} + +// Generate bishop moves +int generate_bishop_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 bishops = side == 0 ? state->bitboards[B] : state->bitboards[b]; + U64 own_pieces = side == 0 ? state->occ_white : state->occ_black; + + while (bishops) { + int from = __builtin_ctzll(bishops); + U64 targets = get_sliding_attacks(from, state->occ_all, 1) & ~own_pieces; + + while (targets) { + int to = __builtin_ctzll(targets); + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(targets, to); + } + + CLEAR_BIT(bishops, from); + } + + return count; +} + +// Generate rook moves +int generate_rook_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 rooks = side == 0 ? state->bitboards[R] : state->bitboards[r]; + U64 own_pieces = side == 0 ? state->occ_white : state->occ_black; + + while (rooks) { + int from = __builtin_ctzll(rooks); + U64 targets = get_sliding_attacks(from, state->occ_all, 0) & ~own_pieces; + + while (targets) { + int to = __builtin_ctzll(targets); + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(targets, to); + } + + CLEAR_BIT(rooks, from); + } + + return count; +} + +// Generate queen moves +int generate_queen_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 queens = side == 0 ? state->bitboards[Q] : state->bitboards[q]; + U64 own_pieces = side == 0 ? state->occ_white : state->occ_black; + + while (queens) { + int from = __builtin_ctzll(queens); + + // Combine bishop and rook moves for queen + U64 bishop_targets = get_sliding_attacks(from, state->occ_all, 1); + U64 rook_targets = get_sliding_attacks(from, state->occ_all, 0); + U64 targets = (bishop_targets | rook_targets) & ~own_pieces; + + while (targets) { + int to = __builtin_ctzll(targets); + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(targets, to); + } + + CLEAR_BIT(queens, from); + } + + return count; +} + +// Generate king moves +int generate_king_moves(const GameState *state, Move *moves, int index) { + int count = index; + int side = state->side_to_move; + U64 king = side == 0 ? state->bitboards[K] : state->bitboards[k]; + U64 own_pieces = side == 0 ? state->occ_white : state->occ_black; + + if (king == 0) return count; // No king (shouldn't happen) + + int from = __builtin_ctzll(king); + U64 targets = king_attacks[from] & ~own_pieces; + + while (targets) { + int to = __builtin_ctzll(targets); + moves[count++] = (Move){from, to, 0, 0}; + CLEAR_BIT(targets, to); + } + + // Castling + if (side == 0) { + // White kingside castling + if ((state->castling & 1) && + !(state->occ_all & 0x0000000000000060ULL) && + !is_square_attacked(state, e1, 1) && + !is_square_attacked(state, f1, 1) && + !is_square_attacked(state, g1, 1)) { + moves[count++] = (Move){e1, g1, 0, 0}; + } + + // White queenside castling + if ((state->castling & 2) && + !(state->occ_all & 0x000000000000000EULL) && + !is_square_attacked(state, e1, 1) && + !is_square_attacked(state, d1, 1) && + !is_square_attacked(state, c1, 1)) { + moves[count++] = (Move){e1, c1, 0, 0}; + } + } else { + // Black kingside castling + if ((state->castling & 4) && + !(state->occ_all & 0x6000000000000000ULL) && + !is_square_attacked(state, e8, 0) && + !is_square_attacked(state, f8, 0) && + !is_square_attacked(state, g8, 0)) { + moves[count++] = (Move){e8, g8, 0, 0}; + } + + // Black queenside castling + if ((state->castling & 8) && + !(state->occ_all & 0x0E00000000000000ULL) && + !is_square_attacked(state, e8, 0) && + !is_square_attacked(state, d8, 0) && + !is_square_attacked(state, c8, 0)) { + moves[count++] = (Move){e8, c8, 0, 0}; + } + } + + return count; +} + +// Generate all pseudo-legal moves +int generate_moves(const GameState *state, Move *moves) { + int count = 0; + count = generate_pawn_moves(state, moves, count); + count = generate_knight_moves(state, moves, count); + count = generate_bishop_moves(state, moves, count); + count = generate_rook_moves(state, moves, count); + count = generate_queen_moves(state, moves, count); + count = generate_king_moves(state, moves, count); + return count; +} + +// Check if a move is legal (doesn't leave king in check) +int is_move_legal(GameState *state, Move move) { + int side = state->side_to_move; + int captured_pc = NO_PIECE; + int old_castling = state->castling; + int old_ep = state->ep_square; + + // Make the move + make_move(state, move, &captured_pc, &old_castling, &old_ep); + + // Check if king is in check after the move + int in_check = is_king_in_check(state, side); + + // Undo the move + undo_move(state, move, captured_pc, old_castling, old_ep); + + return !in_check; +} + +// Generate all legal moves +int generate_legal_moves(GameState *state, Move *moves) { + Move pseudo_moves[256]; + int count = generate_moves(state, pseudo_moves); + int legal_count = 0; + + for (int i = 0; i < count; i++) { + if (is_move_legal(state, pseudo_moves[i])) { + moves[legal_count++] = pseudo_moves[i]; + } + } + + return legal_count; +} \ No newline at end of file diff --git a/src/movegen.h b/src/movegen.h new file mode 100644 index 0000000..d42e880 --- /dev/null +++ b/src/movegen.h @@ -0,0 +1,36 @@ +#ifndef MOVEGEN_H +#define MOVEGEN_H + +#include "types.h" + +// Check if a square is attacked by a given side +int is_square_attacked(const GameState *state, int sq, int side); + +// Generate pawn moves +int generate_pawn_moves(const GameState *state, Move *moves, int index); + +// Generate knight moves +int generate_knight_moves(const GameState *state, Move *moves, int index); + +// Generate bishop moves +int generate_bishop_moves(const GameState *state, Move *moves, int index); + +// Generate rook moves +int generate_rook_moves(const GameState *state, Move *moves, int index); + +// Generate queen moves +int generate_queen_moves(const GameState *state, Move *moves, int index); + +// Generate king moves +int generate_king_moves(const GameState *state, Move *moves, int index); + +// Generate all pseudo-legal moves +int generate_moves(const GameState *state, Move *moves); + +// Check if a move is legal (doesn't leave king in check) +int is_move_legal(GameState *state, Move move); + +// Generate all legal moves +int generate_legal_moves(GameState *state, Move *moves); + +#endif // MOVEGEN_H diff --git a/src/notation.c b/src/notation.c new file mode 100644 index 0000000..1c883a3 --- /dev/null +++ b/src/notation.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include "notation.h" +#include "board.h" +#include "movegen.h" + +// Convert a move to SAN notation +char* move_to_san(const GameState *state, Move move) { + static char san[10]; + int from_file = move.from % 8; + int from_rank = move.from / 8; + int to_file = move.to % 8; + int to_rank = move.to / 8; + + // Find the moving piece + int pc = NO_PIECE; + for (int i = 0; i < 12; i++) { + if (GET_BIT(state->bitboards[i], move.from)) { + pc = i; + break; + } + } + + if (pc == NO_PIECE) return "error"; + + // Handle castling + if ((pc == K || pc == k) && abs(move.to - move.from) == 2) { + if (to_file > from_file) { + strcpy(san, "O-O"); // Kingside castle + } else { + strcpy(san, "O-O-O"); // Queenside castle + } + return san; + } + + // Normal moves + char piece_char = ' '; + if (pc != P && pc != p) { + piece_char = "NBRQK"[pc % 6]; + } + + // Check if capture + int is_capture = 0; + for (int i = 0; i < 12; i++) { + if (i != pc && GET_BIT(state->bitboards[i], move.to)) { + is_capture = 1; + break; + } + } + + // En passant capture + if ((pc == P || pc == p) && move.to == state->ep_square) { + is_capture = 1; + } + + // Check if we need file/rank disambiguation + int need_file = 0, need_rank = 0; + if (pc != P && pc != p) { + // Find if other pieces of same type can move to same square + GameState test_state = *state; + Move test_moves[256]; + int num_moves = generate_legal_moves(&test_state, test_moves); + + for (int i = 0; i < num_moves; i++) { + if (test_moves[i].to == move.to && test_moves[i].from != move.from) { + // See if it's the same piece type + for (int p = 0; p < 12; p++) { + if (GET_BIT(state->bitboards[p], test_moves[i].from) && p == pc) { + int other_file = test_moves[i].from % 8; + int other_rank = test_moves[i].from / 8; + + if (other_file == from_file) { + need_rank = 1; + } else if (other_rank == from_rank) { + need_file = 1; + } else { + need_file = 1; + } + } + } + } + } + } + + // Build the SAN string + int idx = 0; + + // Add piece letter except for pawns + if (piece_char != ' ') { + san[idx++] = piece_char; + } + + // Add disambiguation if needed + if (need_file) { + san[idx++] = 'a' + from_file; + } + if (need_rank) { + san[idx++] = '1' + from_rank; + } + + // For pawns capturing, need to specify the file + if ((pc == P || pc == p) && is_capture) { + san[idx++] = 'a' + from_file; + } + + // Add capture symbol + if (is_capture) { + san[idx++] = 'x'; + } + + // Add destination square + san[idx++] = 'a' + to_file; + san[idx++] = '1' + to_rank; + + // Add promotion + if (move.promo) { + san[idx++] = '='; + san[idx++] = "NBRQ"[move.promo - 1]; + } + + // Check for check or checkmate + GameState after_state = *state; + int captured, old_castling, old_ep; + make_move(&after_state, move, &captured, &old_castling, &old_ep); + + if (is_king_in_check(&after_state, after_state.side_to_move)) { + // Check if it's checkmate + Move next_moves[256]; + int num_next_moves = generate_legal_moves(&after_state, next_moves); + + if (num_next_moves == 0) { + san[idx++] = '#'; // Checkmate + } else { + san[idx++] = '+'; // Check + } + } + + san[idx] = '\0'; + return san; +} + +// Parse SAN notation into a Move +Move parse_san(const GameState *state, const char *san) { + Move result = {0, 0, 0, 0}; + int side = state->side_to_move; + + // Handle castling + if (strcmp(san, "O-O") == 0 || strcmp(san, "0-0") == 0) { + if (side == 0) { + result.from = e1; + result.to = g1; + } else { + result.from = e8; + result.to = g8; + } + return result; + } + + if (strcmp(san, "O-O-O") == 0 || strcmp(san, "0-0-0") == 0) { + if (side == 0) { + result.from = e1; + result.to = c1; + } else { + result.from = e8; + result.to = c8; + } + return result; + } + + // Parse the move + int len = strlen(san); + int idx = 0; + + // Piece type + int piece_type = P; // Default to pawn + if (san[idx] >= 'A' && san[idx] <= 'Z') { + switch (san[idx]) { + case 'N': piece_type = N; break; + case 'B': piece_type = B; break; + case 'R': piece_type = R; break; + case 'Q': piece_type = Q; break; + case 'K': piece_type = K; break; + default: break; + } + idx++; + } + if (side == 1) piece_type += 6; // Adjust for black pieces + + // Source file and rank disambiguation + int source_file = -1, source_rank = -1; + + // Look for disambiguating info before the target or capture symbol + while (idx < len) { + char c = san[idx]; + + if (c >= 'a' && c <= 'h') { + source_file = c - 'a'; + idx++; + } else if (c >= '1' && c <= '8') { + source_rank = c - '1'; + idx++; + } else if (c == 'x') { + // Skip capture symbol + idx++; + } else { + break; + } + } + + // Target square + if (idx + 1 >= len) return result; // Invalid SAN + + int target_file = san[idx] - 'a'; + int target_rank = san[idx + 1] - '1'; + idx += 2; + + if (target_file < 0 || target_file > 7 || target_rank < 0 || target_rank > 7) { + return result; // Invalid target square + } + + int target_square = target_rank * 8 + target_file; + result.to = target_square; + + // Promotion + if (idx < len && san[idx] == '=') { + idx++; + if (idx < len) { + switch (san[idx]) { + case 'N': result.promo = 1; break; + case 'B': result.promo = 2; break; + case 'R': result.promo = 3; break; + case 'Q': result.promo = 4; break; + default: break; + } + idx++; + } + } + + // Find the source square + Move legal_moves[256]; + int num_legal_moves = generate_legal_moves((GameState*)state, legal_moves); + + for (int i = 0; i < num_legal_moves; i++) { + Move m = legal_moves[i]; + + if (m.to == result.to && m.promo == result.promo) { + // Check if this move matches our piece type and any source constraints + int from_square = m.from; + int from_file = from_square % 8; + int from_rank = from_square / 8; + + for (int p = 0; p < 12; p++) { + if (GET_BIT(state->bitboards[p], from_square)) { + if (p == piece_type && + (source_file == -1 || source_file == from_file) && + (source_rank == -1 || source_rank == from_rank)) { + result.from = from_square; + return result; + } + break; + } + } + } + } + + return result; // If no matching move was found +} + +// Print move in algebraic notation +void print_move(const GameState *state, Move move) { + printf("%s", move_to_san(state, move)); +} diff --git a/src/notation.h b/src/notation.h new file mode 100644 index 0000000..971615e --- /dev/null +++ b/src/notation.h @@ -0,0 +1,15 @@ +#ifndef NOTATION_H +#define NOTATION_H + +#include "types.h" + +// Convert a move to SAN notation +char* move_to_san(const GameState *state, Move move); + +// Parse SAN notation into a Move +Move parse_san(const GameState *state, const char *san); + +// Print move in algebraic notation +void print_move(const GameState *state, Move move); + +#endif // NOTATION_H diff --git a/src/search.c b/src/search.c new file mode 100644 index 0000000..2724fe8 --- /dev/null +++ b/src/search.c @@ -0,0 +1,202 @@ +#include +#include "search.h" +#include "evaluation.h" +#include "movegen.h" +#include "board.h" + +// Order moves to improve alpha-beta pruning +void order_moves(Move *moves, int num_moves, const GameState *state) { + // Simple move ordering: + // 1. Captures (with MVV-LVA: Most Valuable Victim - Least Valuable Aggressor) + // 2. Promotions + // 3. Checks + // 4. Other moves + + int scores[256] = {0}; + + for (int i = 0; i < num_moves; i++) { + int from = moves[i].from; + int to = moves[i].to; + int moving_piece = NO_PIECE; + int captured_piece = NO_PIECE; + + // Find the moving piece + for (int p = 0; p < 12; p++) { + if (GET_BIT(state->bitboards[p], from)) { + moving_piece = p; + break; + } + } + + // Find captured piece + for (int p = 0; p < 12; p++) { + if (GET_BIT(state->bitboards[p], to)) { + captured_piece = p; + break; + } + } + + // En passant capture + if ((moving_piece == P || moving_piece == p) && + to == state->ep_square) { + captured_piece = state->side_to_move == 0 ? p : P; + } + + // Base score + if (captured_piece != NO_PIECE) { + // MVV-LVA: score = 10 * victim_value - attacker_value + int victim_value = abs(piece_value[captured_piece]) / 100; + int attacker_value = abs(piece_value[moving_piece]) / 100; + scores[i] = 10 * victim_value - attacker_value + 1000; + } + + // Promotion bonus + if (moves[i].promo) { + scores[i] += 800 + moves[i].promo * 10; // Queen promo gets highest score + } + + // Check if move gives check (simplified, could be more efficient) + GameState check_state = *state; + int captured, old_castling, old_ep; + make_move(&check_state, moves[i], &captured, &old_castling, &old_ep); + if (is_king_in_check(&check_state, check_state.side_to_move)) { + scores[i] += 500; + } + } + + // Sort moves based on scores (simple insertion sort) + for (int i = 1; i < num_moves; i++) { + Move temp_move = moves[i]; + int temp_score = scores[i]; + int j = i - 1; + + while (j >= 0 && scores[j] < temp_score) { + moves[j + 1] = moves[j]; + scores[j + 1] = scores[j]; + j--; + } + + moves[j + 1] = temp_move; + scores[j + 1] = temp_score; + } +} + +// Negamax with alpha-beta pruning +int minimax(GameState *state, int depth, int alpha, int beta) { + // Check if we're at a leaf node + if (depth == 0) { + return evaluate(state); + } + + // Generate and count legal moves + Move moves[256]; + int num_moves = generate_legal_moves(state, moves); + + // Check for checkmate or stalemate + if (num_moves == 0) { + if (is_king_in_check(state, state->side_to_move)) { + return -30000 + (10 - depth); // Checkmate (prefer faster checkmate) + } else { + return 0; // Stalemate + } + } + + // Order moves to improve alpha-beta pruning + order_moves(moves, num_moves, state); + + int best_score = -32000; + + // Try each move + for (int i = 0; i < num_moves; i++) { + int captured, old_castling, old_ep; + + make_move(state, moves[i], &captured, &old_castling, &old_ep); + int score = -minimax(state, depth - 1, -beta, -alpha); + undo_move(state, moves[i], captured, old_castling, old_ep); + + if (score > best_score) { + best_score = score; + } + + if (best_score > alpha) { + alpha = best_score; + } + + if (alpha >= beta) { + break; // Beta cutoff + } + } + + return best_score; +} + +// Thread function for search +void *thread_search(void *arg) { + ThreadArg *thread_arg = (ThreadArg *)arg; + GameState *state = &thread_arg->state; + int captured, old_castling, old_ep; + + // Make the move + make_move(state, thread_arg->move, &captured, &old_castling, &old_ep); + + // Search from this position + thread_arg->eval = -minimax(state, thread_arg->depth - 1, -30000, 30000); + + // Lock mutex before checking if this is the best move + pthread_mutex_lock(thread_arg->mutex); + + if (thread_arg->eval > *(thread_arg->best_eval)) { + *(thread_arg->best_eval) = thread_arg->eval; + *(thread_arg->best_move) = thread_arg->move; + } + + pthread_mutex_unlock(thread_arg->mutex); + + return NULL; +} + +// Find best move using multithreaded search +Move find_best_move(GameState *state, int depth) { + Move moves[256]; + int num_moves = generate_legal_moves(state, moves); + + if (num_moves == 0) { + // No legal moves + return (Move){0, 0, 0, 0}; + } + + // Order moves to search promising moves first + order_moves(moves, num_moves, state); + + // Set up threads + pthread_t threads[256]; + ThreadArg thread_args[256]; + + // Shared variables for best move + pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + int best_eval = -32000; + Move best_move = moves[0]; // Default to first move + + // Start a thread for each move + for (int i = 0; i < num_moves; i++) { + // Make a copy of the game state for each thread + thread_args[i].state = *state; + thread_args[i].move = moves[i]; + thread_args[i].depth = depth; + thread_args[i].mutex = &mutex; + thread_args[i].best_eval = &best_eval; + thread_args[i].best_move = &best_move; + + pthread_create(&threads[i], NULL, thread_search, &thread_args[i]); + } + + // Wait for all threads to finish + for (int i = 0; i < num_moves; i++) { + pthread_join(threads[i], NULL); + } + + // Clean up mutex + pthread_mutex_destroy(&mutex); + + return best_move; +} \ No newline at end of file diff --git a/src/search.h b/src/search.h new file mode 100644 index 0000000..59991d2 --- /dev/null +++ b/src/search.h @@ -0,0 +1,19 @@ +#ifndef SEARCH_H +#define SEARCH_H + +#include +#include "types.h" + +// Order moves to improve alpha-beta pruning +void order_moves(Move *moves, int num_moves, const GameState *state); + +// Negamax with alpha-beta pruning +int minimax(GameState *state, int depth, int alpha, int beta); + +// Thread function for search +void *thread_search(void *arg); + +// Find best move using multithreaded search +Move find_best_move(GameState *state, int depth); + +#endif // SEARCH_H diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..4a842ed --- /dev/null +++ b/src/types.h @@ -0,0 +1,66 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +// Type for bitboard +typedef uint64_t U64; + +// Piece indices +enum Piece { + P, N, B, R, Q, K, // White: pawn, knight, bishop, rook, queen, king + p, n, b, r, q, k, // Black + NO_PIECE +}; + +// Squares +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, + NO_SQ +}; + +// Move structure +typedef struct { + int from; + int to; + int promo; // promotion piece or 0 + int score; // for move ordering +} Move; + +// Game state +typedef struct { + U64 bitboards[12]; + U64 occ_white, occ_black, occ_all; + int side_to_move; // 0 white, 1 black + int castling; // KQkq permissions (1-8) + int ep_square; // en passant target square + int halfmove_clock; // for 50-move rule + int fullmove_number; // starts at 1, increments after black's move +} GameState; + +// Thread argument structure +typedef struct { + GameState state; + Move move; + int depth; + int eval; + pthread_mutex_t *mutex; + int *best_eval; + Move *best_move; +} ThreadArg; + +// Helper macros +#define GET_BIT(bb, sq) (((bb) >> (sq)) & 1ULL) +#define SET_BIT(bb, sq) ((bb) |= (1ULL << (sq))) +#define CLEAR_BIT(bb, sq) ((bb) &= ~(1ULL << (sq))) +#define SQUARE(rank, file) ((rank) * 8 + (file)) + +#endif // TYPES_H