20-add-negamax-eval-function (#31)
All checks were successful
Python tests (make) / test (push) Successful in 12s

Reviewed-on: #31
Co-authored-by: Josh <josh@joshuaschuett.com>
Co-committed-by: Josh <josh@joshuaschuett.com>
This commit is contained in:
2025-08-26 00:28:03 +00:00
committed by Josh
parent 6005741b10
commit 27d0f1b6e6
12 changed files with 622 additions and 24 deletions

115
engine/src/ai/negamax.c Normal file
View File

@@ -0,0 +1,115 @@
#include <stdlib.h>
#include <time.h>
#include <limits.h>
#include "ai.h"
#include "bitboard.h"
#define INF 30000
#define MATE 29000
// P N B R Q K
int VAL[6] = {100, 300, 300, 500, 900, 0};
int popcount64(uint64_t bits) {
int count = 0;
while (bits) {
// clear lowest set bit.
bits &= (bits - 1);
++count;
}
return count;
}
static int eval(struct Board *board) {
// White minus Black material (centipawns)
// Ignore the King's material.
int score = 0;
score += VAL[0] * (popcount64(board->pieces[P]) - popcount64(board->pieces[p]));
score += VAL[1] * (popcount64(board->pieces[N]) - popcount64(board->pieces[n]));
score += VAL[2] * (popcount64(board->pieces[B]) - popcount64(board->pieces[b]));
score += VAL[3] * (popcount64(board->pieces[R]) - popcount64(board->pieces[r]));
score += VAL[4] * (popcount64(board->pieces[Q]) - popcount64(board->pieces[q]));
// Negamax convention: score from side-to-move POV
return (board->side_to_move == WHITE) ? score : -score;
}
static int search(struct Board *b, int depth, int alpha, int beta, int ply) {
if (depth == 0) return eval(b);
struct Move moves[256];
int n = get_legal_moves((struct Board*)b, moves);
if (n == 0) {
// terminal: checkmate or stalemate
// prefer quicker mates
if (in_check(b, b->side_to_move)) {
return -MATE + ply;
}
// Stalemate
return 0;
}
int best = -INF;
for (int i = 0; i < n; ++i) {
struct Board after;
if (!apply_move_on_copy((struct Board*)b, &after, moves[i])) continue;
int score = -search(&after, depth - 1, -beta, -alpha, ply + 1);
if (score > best) best = score;
if (score > alpha) alpha = score;
if (alpha >= beta) break; // cutoff
}
return best;
}
int rand_uniform(int upper_bound) {
if (upper_bound <= 0) return 0;
// Rejection sampling to reduce modulo bias
int limit = RAND_MAX - (RAND_MAX % upper_bound);
int r;
do { r = rand(); } while (r > limit);
return r % upper_bound;
}
int ai_find_best_move_with_window(struct Board *board, int depth, int window_cp, struct Move *best_out) {
enum { MAX_MOVES = 256 };
struct Move moves[MAX_MOVES];
int n = get_legal_moves(board, moves);
if (n <= 0) return 0;
int scores[MAX_MOVES];
int best = -INF;
for (int i = 0; i < n; ++i) {
struct Board after;
if (!apply_move_on_copy(board, &after, moves[i])) return 0;
// Root child: depth-1; alpha=-INF, beta=+INF; ply starts at 1
int s = -search(&after, depth - 1, -INF, INF, 1);
scores[i] = s;
if (s > best) best = s;
}
// collect candidates within window
int cand_idx[MAX_MOVES], cand_n = 0;
for (int i = 0; i < n; ++i)
if (scores[i] >= best - window_cp)
cand_idx[cand_n++] = i;
// fallback if window excludes all
if (cand_n == 0) {
int best_i = 0;
for (int i = 1; i < n; ++i)
if (scores[i] > scores[best_i]) best_i = i;
*best_out = moves[best_i];
return 1;
}
// Select a random candidate to prevent deterministic results.
// I found that games were coming out with the same result each
// time when the AI played itself.
int pick = cand_idx[rand_uniform(cand_n)];
*best_out = moves[pick];
return 1;
}

View File

@@ -715,6 +715,14 @@ bool apply_move_on_copy(struct Board *in, struct Board *out, struct Move m) {
in->castling_rights, m.piece, m.from, m.to, captured_pid
);
// Apply full and half move clock counts.
bool is_capture = (m.flags & MF_CAPTURE) || (m.flags & MF_ENPASSANT);
bool is_pawn = (m.piece == P) || (m.piece == p);
out->halfmove_clock = (is_capture || is_pawn) ? 0 : in->halfmove_clock + 1;
out->fullmove_number = in->fullmove_number + (in->side_to_move == BLACK);
// Side to move flips
out->side_to_move = opp;

View File

@@ -10,6 +10,11 @@ int square_index(int file, int rank) {
return rank*8 + file;
}
static const char PIECE_CH[12] = {
'P','N','B','R','Q','K',
'p','n','b','r','q','k'
};
int char_to_piece_index(char c) {
switch (c) {
case 'P': return P; case 'N': return N; case 'B': return B;
@@ -117,4 +122,73 @@ int load_fen(struct Board *board, const char *fen) {
board->occ[BOTH] = board->occ[WHITE] | board->occ[BLACK];
return 0;
}
void board_to_fen(const struct Board *board, char *out, size_t out_sz) {
char fen[256];
char *p = fen;
// 1. Piece placement
for (int rank = 7; rank >= 0; rank--) {
int empty = 0;
for (int file = 0; file < 8; file++) {
int sq = rank * 8 + file;
char piece = 0;
for (int i = 0; i < 12; i++) {
if (board->pieces[i] & (1ULL << sq)) {
piece = PIECE_CH[i];
break;
}
}
if (piece) {
if (empty > 0) {
*p++ = '0' + empty;
empty = 0;
}
*p++ = piece;
} else {
empty++;
}
}
if (empty > 0) *p++ = '0' + empty;
if (rank > 0) *p++ = '/';
}
// 2. Side to move
*p++ = ' ';
*p++ = (board->side_to_move == WHITE) ? 'w' : 'b';
// 3. Castling rights
*p++ = ' ';
bool any = false;
if (board->castling_rights & CASTLE_WK) { *p++ = 'K'; any = true; }
if (board->castling_rights & CASTLE_WQ) { *p++ = 'Q'; any = true; }
if (board->castling_rights & CASTLE_BK) { *p++ = 'k'; any = true; }
if (board->castling_rights & CASTLE_BQ) { *p++ = 'q'; any = true; }
if (!any) *p++ = '-';
// 4. En passant
*p++ = ' ';
if (board->ep_square >= 0) {
int file = board->ep_square % 8;
int rank = board->ep_square / 8;
*p++ = 'a' + file;
*p++ = '1' + rank;
} else {
*p++ = '-';
}
// 5. Halfmove clock
p += sprintf(p, " %d", board->halfmove_clock);
// 6. Fullmove number
p += sprintf(p, " %d", board->fullmove_number);
*p = '\0';
// Copy safely into output buffer
strncpy(out, fen, out_sz);
out[out_sz - 1] = '\0';
}