20-add-negamax-eval-function #31
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
build
|
build
|
||||||
|
data
|
||||||
__pycache__
|
__pycache__
|
||||||
@@ -28,6 +28,9 @@ FILES = {c:i for i,c in enumerate("abcdefgh")}
|
|||||||
WHITE, BLACK, BOTH = 0, 1, 2
|
WHITE, BLACK, BOTH = 0, 1, 2
|
||||||
P, N, B, R, Q, K, p, n, b, r, q, k = range(12)
|
P, N, B, R, Q, K, p, n, b, r, q, k = range(12)
|
||||||
|
|
||||||
|
PIECE_CH = ('P','N','B','R','Q','K','p','n','b','r','q','k')
|
||||||
|
FILES_STR = "a b c d e f g h"
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Register C function bindings and interfaces in a dictionary.
|
Register C function bindings and interfaces in a dictionary.
|
||||||
@@ -58,6 +61,11 @@ FFI_SPEC = {
|
|||||||
"perft": ((C.POINTER(Board), C.c_int), C.c_uint64),
|
"perft": ((C.POINTER(Board), C.c_int), C.c_uint64),
|
||||||
"load_fen": ((C.POINTER(Board), C.c_char_p), C.c_int),
|
"load_fen": ((C.POINTER(Board), C.c_char_p), C.c_int),
|
||||||
"gen_pseudo_moves": ((C.POINTER(Board), C.POINTER(Move), C.c_bool), C.c_int),
|
"gen_pseudo_moves": ((C.POINTER(Board), C.POINTER(Move), C.c_bool), C.c_int),
|
||||||
|
"apply_move_on_copy": ((C.POINTER(Board), C.POINTER(Board), Move), C.c_bool),
|
||||||
|
"board_to_fen": ((C.POINTER(Board), C.c_char_p, C.c_size_t), None),
|
||||||
|
|
||||||
|
# AI Methods
|
||||||
|
"ai_find_best_move_with_window": ((C.POINTER(Board), C.c_int, C.c_int, C.POINTER(Move)), C.c_int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -82,6 +90,28 @@ def draw_bb(mask, origin=None):
|
|||||||
print(lines, "\n")
|
print(lines, "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def print_board(board):
|
||||||
|
"""
|
||||||
|
Prints the current position using piece letters from board.pieces.
|
||||||
|
highlight_sq: optional 0..63 square index to mark with 'O'.
|
||||||
|
"""
|
||||||
|
def piece_at(sq: int) -> str:
|
||||||
|
for i, ch in enumerate(PIECE_CH):
|
||||||
|
if (int(board.pieces[i]) >> sq) & 1:
|
||||||
|
return ch
|
||||||
|
return '.'
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for r in range(7, -1, -1):
|
||||||
|
row = []
|
||||||
|
for f in range(8):
|
||||||
|
sq = r * 8 + f
|
||||||
|
row.append(piece_at(sq))
|
||||||
|
lines.append(f"{r+1} " + " ".join(row))
|
||||||
|
lines.append(" " + FILES_STR)
|
||||||
|
print("\n" + "\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def sq_to_coord(sq):
|
def sq_to_coord(sq):
|
||||||
return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8))
|
return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8))
|
||||||
|
|
||||||
@@ -125,12 +155,22 @@ class ChessFFI:
|
|||||||
return self._c_load_fen(board, fen_string.encode("ascii"))
|
return self._c_load_fen(board, fen_string.encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
def board_to_fen(self, board, size=256):
|
||||||
|
buf = C.create_string_buffer(size)
|
||||||
|
self._c_board_to_fen(board, buf, size)
|
||||||
|
return buf.value.decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
def gen_pseudo_moves(self, board, captures_only=False, cap=256):
|
def gen_pseudo_moves(self, board, captures_only=False, cap=256):
|
||||||
buf = (Move * cap)()
|
buf = (Move * cap)()
|
||||||
n = self._c_gen_pseudo_moves(board, buf, captures_only)
|
n = self._c_gen_pseudo_moves(board, buf, captures_only)
|
||||||
return buf, n
|
return buf, n
|
||||||
|
|
||||||
|
|
||||||
|
def apply_move_on_copy(self, board, out_board, move):
|
||||||
|
return bool(self._c_apply_move_on_copy(C.byref(board), C.byref(out_board), move))
|
||||||
|
|
||||||
|
|
||||||
def get_legal_moves(self, board, out):
|
def get_legal_moves(self, board, out):
|
||||||
return self._c_get_legal_moves(board, out)
|
return self._c_get_legal_moves(board, out)
|
||||||
|
|
||||||
|
|||||||
4
engine/include/ai.h
Normal file
4
engine/include/ai.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#include "bitboard.h"
|
||||||
|
|
||||||
|
int ai_find_best_move_with_window(struct Board *b, int depth, int window_cp, struct Move *best_out);
|
||||||
|
int ai_play(struct Board *b, int depth);
|
||||||
@@ -115,3 +115,6 @@ void gen_black_pawn_captures(const struct Board *board, struct Move *moves, int
|
|||||||
void gen_white_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count);
|
void gen_white_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count);
|
||||||
void gen_black_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count);
|
void gen_black_pawn_capture_promotions(const struct Board *board, struct Move *moves, int *count);
|
||||||
int gen_pseudo_moves(const struct Board *board, struct Move *out, bool captures_only);
|
int gen_pseudo_moves(const struct Board *board, struct Move *out, bool captures_only);
|
||||||
|
int get_legal_moves(struct Board *board, struct Move *out);
|
||||||
|
bool in_check(const struct Board *board, enum Color side);
|
||||||
|
bool apply_move_on_copy(struct Board *in, struct Board *out, struct Move m);
|
||||||
@@ -1 +1,2 @@
|
|||||||
int load_fen();
|
int load_fen();
|
||||||
|
void board_to_fen(struct Board *board, char *out, size_t out_sz);
|
||||||
|
|||||||
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
|
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
|
// Side to move flips
|
||||||
out->side_to_move = opp;
|
out->side_to_move = opp;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ int square_index(int file, int rank) {
|
|||||||
return rank*8 + file;
|
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) {
|
int char_to_piece_index(char c) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'P': return P; case 'N': return N; case 'B': return B;
|
case 'P': return P; case 'N': return N; case 'B': return B;
|
||||||
@@ -118,3 +123,72 @@ int load_fen(struct Board *board, const char *fen) {
|
|||||||
|
|
||||||
return 0;
|
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';
|
||||||
|
}
|
||||||
54
makefile
54
makefile
@@ -2,46 +2,64 @@ CC := gcc
|
|||||||
CFLAGS := -O3 -fPIC -Wall -Wextra
|
CFLAGS := -O3 -fPIC -Wall -Wextra
|
||||||
LDFLAGS := -shared
|
LDFLAGS := -shared
|
||||||
|
|
||||||
SRCDIR := engine/src
|
# ---- paths ----
|
||||||
INCDIR := engine/include
|
SRCDIRS := engine/src engine/src/ai
|
||||||
|
INCDIRS := engine/include
|
||||||
BUILDDIR := build
|
BUILDDIR := build
|
||||||
LIBNAME := libchess
|
LIBNAME := libchess
|
||||||
|
|
||||||
# Exclude test.c from the shared lib build
|
|
||||||
SRC := $(filter-out $(SRCDIR)/test.c,$(wildcard $(SRCDIR)/*.c))
|
|
||||||
OBJ := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRC))
|
|
||||||
LIB := $(BUILDDIR)/$(LIBNAME).so
|
LIB := $(BUILDDIR)/$(LIBNAME).so
|
||||||
|
|
||||||
|
# ---- sources/objects ----
|
||||||
|
EXCLUDE := engine/src/test.c
|
||||||
|
SRCS := $(foreach d,$(SRCDIRS),$(wildcard $(d)/*.c))
|
||||||
|
SRCS := $(filter-out $(EXCLUDE),$(SRCS))
|
||||||
|
OBJS := $(patsubst %.c,$(BUILDDIR)/%.o,$(SRCS))
|
||||||
|
|
||||||
|
# ---- includes ----
|
||||||
|
INCLUDES := $(addprefix -I,$(INCDIRS))
|
||||||
|
|
||||||
# ---- test executable (engine/src/test.c) ----
|
# ---- test executable (engine/src/test.c) ----
|
||||||
TESTSRC := $(SRCDIR)/test.c
|
TESTSRC := engine/src/test.c
|
||||||
TESTOBJ := $(BUILDDIR)/test.o
|
TESTOBJ := $(BUILDDIR)/engine/src/test.o
|
||||||
TESTBIN := $(BUILDDIR)/print_board
|
TESTBIN := $(BUILDDIR)/print_board
|
||||||
|
|
||||||
.PHONY: all clean test test-exe run-c-test
|
# ---- runner (python) ----
|
||||||
|
PYTHON := python3
|
||||||
|
RUN_SCRIPT := scripts/simulation.py
|
||||||
|
ARGS ?=
|
||||||
|
|
||||||
|
.PHONY: all clean test test-exe run-c-test run-engine
|
||||||
|
|
||||||
all: $(LIB)
|
all: $(LIB)
|
||||||
|
|
||||||
$(BUILDDIR):
|
# generic object rule: build/<path>/file.o from <path>/file.c
|
||||||
@mkdir -p $(BUILDDIR)
|
$(BUILDDIR)/%.o: %.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||||
|
|
||||||
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
|
$(LIB): $(OBJS)
|
||||||
$(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@
|
@mkdir -p $(dir $@)
|
||||||
|
|
||||||
$(LIB): $(OBJ) | $(BUILDDIR)
|
|
||||||
$(CC) $(LDFLAGS) -o $@ $^
|
$(CC) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
# ---- test exe rules ----
|
# ---- test exe rules ----
|
||||||
test-exe: $(TESTBIN)
|
test-exe: $(TESTBIN)
|
||||||
|
|
||||||
$(TESTOBJ): $(TESTSRC) | $(BUILDDIR)
|
$(TESTOBJ): $(TESTSRC)
|
||||||
$(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||||
|
|
||||||
$(TESTBIN): $(TESTOBJ) $(LIB) | $(BUILDDIR)
|
$(TESTBIN): $(TESTOBJ) $(LIB)
|
||||||
$(CC) -O2 -o $@ $(TESTOBJ) -L$(BUILDDIR) -lchess -Wl,-rpath,'$$ORIGIN'
|
$(CC) -O2 -o $@ $(TESTOBJ) -L$(BUILDDIR) -lchess -Wl,-rpath,'$$ORIGIN'
|
||||||
|
|
||||||
run-c-test: $(TESTBIN)
|
run-c-test: $(TESTBIN)
|
||||||
LD_LIBRARY_PATH=$(BUILDDIR) $(TESTBIN) $(FEN)
|
LD_LIBRARY_PATH=$(BUILDDIR) $(TESTBIN) $(FEN)
|
||||||
|
|
||||||
|
# ---- run engine (Python) ----
|
||||||
|
# Usage: make run-engine ARGS="--fen '...' --depth 5"
|
||||||
|
run-engine: $(LIB)
|
||||||
|
@echo "Running engine via $(RUN_SCRIPT)"
|
||||||
|
LD_LIBRARY_PATH=$(BUILDDIR) $(PYTHON) $(RUN_SCRIPT) $(ARGS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build and Python caches..."
|
@echo "Cleaning build and Python caches..."
|
||||||
@rm -rf $(BUILDDIR)
|
@rm -rf $(BUILDDIR)
|
||||||
|
|||||||
71
scripts/evaluation.py
Normal file
71
scripts/evaluation.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import random
|
||||||
|
from binding.python_c_ffi import ChessFFI
|
||||||
|
from binding.python_c_ffi import Move
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEvaluation:
|
||||||
|
NAME = ""
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, chess_ffi=None):
|
||||||
|
if chess_ffi is None:
|
||||||
|
chess_ffi = ChessFFI()
|
||||||
|
self.chess_ffi = chess_ffi
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_move(self, board, legal_moves):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
We will use a random move evaluation as our base AI. This
|
||||||
|
is expected to be the worst performing strategy. We can
|
||||||
|
play our other AI evaluation methods against this one to
|
||||||
|
confirm if our other strategies are at least better than
|
||||||
|
a simplistic approach.
|
||||||
|
"""
|
||||||
|
class RandomEval(BaseEvaluation):
|
||||||
|
NAME = "random"
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, chess_ffi=None):
|
||||||
|
super().__init__(chess_ffi)
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_move(self, board, legal_moves):
|
||||||
|
return random.choice(legal_moves)
|
||||||
|
|
||||||
|
|
||||||
|
class NegaMaxEval(BaseEvaluation):
|
||||||
|
NAME = "negamax"
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, depth=6, cp_window=20, chess_ffi=None):
|
||||||
|
super().__init__(chess_ffi)
|
||||||
|
self.depth = depth
|
||||||
|
self.window = cp_window
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_move(self, board, legal_moves):
|
||||||
|
best = Move()
|
||||||
|
ok = self.chess_ffi._c_ai_find_best_move_with_window(
|
||||||
|
board,
|
||||||
|
self.depth,
|
||||||
|
self.window,
|
||||||
|
best
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
return best
|
||||||
|
return legal_moves[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
return {
|
||||||
|
"depth": self.depth,
|
||||||
|
"window": self.window,
|
||||||
|
}
|
||||||
81
scripts/format.py
Normal file
81
scripts/format.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
A class to format a series of chess moves into specific formats for
|
||||||
|
storage. The base class expects a series of UCI moves in order. The
|
||||||
|
formatters format the UCI moves into specific output notations for
|
||||||
|
other systems to load and analyze.
|
||||||
|
"""
|
||||||
|
class BaseGameFormatter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
save_path=None,
|
||||||
|
engine="",
|
||||||
|
white="",
|
||||||
|
black="",
|
||||||
|
result="",
|
||||||
|
strategies=None
|
||||||
|
):
|
||||||
|
self.save_path = save_path
|
||||||
|
self.engine = engine
|
||||||
|
self.white = white
|
||||||
|
self.black = black
|
||||||
|
self.strategies = strategies
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
self.formatted_moves = []
|
||||||
|
self.fens = []
|
||||||
|
|
||||||
|
|
||||||
|
def save_game(self, id, moves, fens):
|
||||||
|
self.formatted_moves = self.format_moves(moves)
|
||||||
|
self.fens = fens
|
||||||
|
self.save_to_disk(id)
|
||||||
|
|
||||||
|
|
||||||
|
def format_moves(self, moves):
|
||||||
|
"""
|
||||||
|
Define in child class.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_disk(self, id):
|
||||||
|
date = datetime.today().strftime('%Y-%m-%d')
|
||||||
|
filename = f"{date}_{id}.json"
|
||||||
|
filepath = os.path.join(self.save_path, filename)
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"date": datetime.today().strftime('%Y-%m-%d'),
|
||||||
|
"id": id,
|
||||||
|
"engine": "",
|
||||||
|
"white": "",
|
||||||
|
"black": "",
|
||||||
|
"strategies": self.strategies,
|
||||||
|
"result": self.result,
|
||||||
|
"len_moves": len(self.formatted_moves),
|
||||||
|
"moves": self.formatted_moves,
|
||||||
|
"fens": self.fens
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, "w") as out:
|
||||||
|
json.dump(data, out, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def print_moves(self):
|
||||||
|
for move in self.formatted_moves:
|
||||||
|
print(move)
|
||||||
|
|
||||||
|
|
||||||
|
class LongPGNFormatter(BaseGameFormatter):
|
||||||
|
def format_moves(self, moves):
|
||||||
|
pgn_long = []
|
||||||
|
it = iter(moves)
|
||||||
|
for white in it:
|
||||||
|
black = next(it, None)
|
||||||
|
pgn_long.append(f"{white} {black}" if black else white)
|
||||||
|
return pgn_long
|
||||||
182
scripts/simulation.py
Normal file
182
scripts/simulation.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import uuid
|
||||||
|
from binding.python_c_ffi import Board
|
||||||
|
from binding.python_c_ffi import Move
|
||||||
|
from binding.python_c_ffi import ChessFFI
|
||||||
|
from binding.python_c_ffi import WHITE
|
||||||
|
from binding.python_c_ffi import print_board
|
||||||
|
from binding.python_c_ffi import sq_to_coord
|
||||||
|
from scripts.evaluation import RandomEval
|
||||||
|
from scripts.evaluation import NegaMaxEval
|
||||||
|
from scripts.format import LongPGNFormatter
|
||||||
|
|
||||||
|
|
||||||
|
YMD_HM = "%Y-%m-%d-%H-%M"
|
||||||
|
MAX_MOVES = 256
|
||||||
|
DATA_PATH = "./data/games"
|
||||||
|
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
def __init__(self, strat_white=None, strat_black=None, max_plys=500):
|
||||||
|
self.chess_ffi = ChessFFI()
|
||||||
|
self.board = Board()
|
||||||
|
self.max_plys = max_plys
|
||||||
|
self.moves = []
|
||||||
|
self.fens = []
|
||||||
|
self._load_attack_cache()
|
||||||
|
self._seed_engine()
|
||||||
|
|
||||||
|
random_strat = RandomEval(chess_ffi=self.chess_ffi)
|
||||||
|
|
||||||
|
if not strat_white:
|
||||||
|
self.strat_white = random_strat
|
||||||
|
else:
|
||||||
|
self.strat_white = strat_white
|
||||||
|
|
||||||
|
if not strat_black:
|
||||||
|
self.strat_black = random_strat
|
||||||
|
else:
|
||||||
|
self.strat_black = strat_black
|
||||||
|
|
||||||
|
self.strategies = {
|
||||||
|
"white": {
|
||||||
|
"name": self.strat_white.NAME,
|
||||||
|
"params": self.strat_white.get_params()
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"name": self.strat_black.NAME,
|
||||||
|
"params": self.strat_black.get_params()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run(self, fen, save=True):
|
||||||
|
self.chess_ffi.load_fen(self.board, fen)
|
||||||
|
self._clear_moves()
|
||||||
|
plys = 0
|
||||||
|
|
||||||
|
self.fens.append(fen)
|
||||||
|
while plys <= self.max_plys:
|
||||||
|
print_board(self.board)
|
||||||
|
|
||||||
|
moves_buf = (Move * MAX_MOVES)()
|
||||||
|
n_legal = self.chess_ffi.get_legal_moves(self.board, moves_buf)
|
||||||
|
|
||||||
|
game_over, result = self._is_game_over(n_legal)
|
||||||
|
if game_over:
|
||||||
|
print(result)
|
||||||
|
break
|
||||||
|
|
||||||
|
legal = [moves_buf[i] for i in range(n_legal)]
|
||||||
|
if self.board.side_to_move == WHITE:
|
||||||
|
best_move = self.strat_white.get_best_move(self.board, legal)
|
||||||
|
else:
|
||||||
|
best_move = self.strat_black.get_best_move(self.board, legal)
|
||||||
|
|
||||||
|
new_board = Board()
|
||||||
|
if not self.chess_ffi.apply_move_on_copy(self.board, new_board, best_move):
|
||||||
|
print("ERROR: apply_move_on_copy failed")
|
||||||
|
break
|
||||||
|
|
||||||
|
self.board = new_board
|
||||||
|
plys += 1
|
||||||
|
|
||||||
|
fen_string = self.chess_ffi.board_to_fen(self.board)
|
||||||
|
self.fens.append(fen_string)
|
||||||
|
|
||||||
|
move = self.to_uci(best_move)
|
||||||
|
self.moves.append(move)
|
||||||
|
|
||||||
|
if self.board.halfmove_clock >= 100:
|
||||||
|
print("draw (50-move rule)")
|
||||||
|
result = "1/2-1/2"
|
||||||
|
break
|
||||||
|
|
||||||
|
print("done")
|
||||||
|
print(len(self.moves))
|
||||||
|
|
||||||
|
if save:
|
||||||
|
print("saving game to disk.")
|
||||||
|
id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
formatter = LongPGNFormatter(
|
||||||
|
save_path=DATA_PATH,
|
||||||
|
strategies=self.strategies,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
|
formatter.save_game(id, self.moves, self.fens)
|
||||||
|
|
||||||
|
|
||||||
|
def side_tag(self, side):
|
||||||
|
return 'w' if side == WHITE else 'b'
|
||||||
|
|
||||||
|
|
||||||
|
def load_fen(self, fen):
|
||||||
|
self.chess_ffi.load_fen(self.board, fen)
|
||||||
|
|
||||||
|
|
||||||
|
def to_uci(self, move):
|
||||||
|
fr = sq_to_coord(getattr(move, "from"))
|
||||||
|
to = sq_to_coord(move.to)
|
||||||
|
|
||||||
|
# Only add a promotion letter if promo is set
|
||||||
|
promo_letter = ""
|
||||||
|
promo_val = int(getattr(move, "promo", 0) or 0)
|
||||||
|
if promo_val:
|
||||||
|
# Normalize piece id to type 0..5 (P,N,B,R,Q,K)
|
||||||
|
pt = promo_val % 6
|
||||||
|
# Map N=1, B=2, R=3, Q=4
|
||||||
|
letter_map = {1: "n", 2: "b", 3: "r", 4: "q"}
|
||||||
|
promo_letter = letter_map.get(pt, "")
|
||||||
|
|
||||||
|
return f"{fr}{to}{promo_letter}"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_attack_cache(self):
|
||||||
|
self.chess_ffi.init_attack_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_moves(self):
|
||||||
|
self.moves = []
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_engine(self):
|
||||||
|
import ctypes as C, time
|
||||||
|
|
||||||
|
seed = time.time_ns()
|
||||||
|
s32 = int(seed) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
libc = C.CDLL("libc.so.6") # on Ubuntu
|
||||||
|
libc.srand.argtypes = (C.c_uint,)
|
||||||
|
libc.srand.restype = None
|
||||||
|
libc.srand(C.c_uint(s32))
|
||||||
|
return s32
|
||||||
|
|
||||||
|
|
||||||
|
def _is_game_over(self, legal_moves):
|
||||||
|
if legal_moves == 0:
|
||||||
|
if self.chess_ffi.in_check(self.board, self.board.side_to_move):
|
||||||
|
# Checkmate.
|
||||||
|
if self.board.side_to_move == WHITE:
|
||||||
|
result = "0-1"
|
||||||
|
else:
|
||||||
|
result = "1-0"
|
||||||
|
else:
|
||||||
|
# Stalemate.
|
||||||
|
result = "1/2-1/2"
|
||||||
|
return True, result
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
chess_ffi = ChessFFI()
|
||||||
|
nega_strat_1 = NegaMaxEval(chess_ffi=chess_ffi, depth=2, cp_window=5)
|
||||||
|
nega_strat_2 = NegaMaxEval(chess_ffi=chess_ffi, depth=1, cp_window=5)
|
||||||
|
|
||||||
|
|
||||||
|
engine = Engine(
|
||||||
|
strat_white=nega_strat_1,
|
||||||
|
strat_black=nega_strat_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
|
engine.run(fen, save=True)
|
||||||
Reference in New Issue
Block a user