20-add-negamax-eval-function (#31)
All checks were successful
Python tests (make) / test (push) Successful in 12s
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:
115
engine/src/ai/negamax.c
Normal file
115
engine/src/ai/negamax.c
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user