Add some basic algorithms and eval functions for an AI
All checks were successful
Python tests (make) / test (push) Successful in 11s

This commit is contained in:
2025-08-23 13:10:05 -04:00
parent 60a987a7a1
commit 4fc982eac2
6 changed files with 409 additions and 21 deletions

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

@@ -0,0 +1,123 @@
#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;
}
int ai_play(struct Board *b, int depth) {
struct Move m;
struct Board after;
if (!apply_move_on_copy(b, &after, m)) return 0;
*b = after;
return 1;
}