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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
build
|
||||
__pycache__
|
||||
data
|
||||
__pycache__
|
||||
|
||||
@@ -28,6 +28,9 @@ FILES = {c:i for i,c in enumerate("abcdefgh")}
|
||||
WHITE, BLACK, BOTH = 0, 1, 2
|
||||
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.
|
||||
@@ -58,6 +61,11 @@ FFI_SPEC = {
|
||||
"perft": ((C.POINTER(Board), C.c_int), C.c_uint64),
|
||||
"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),
|
||||
"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)
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +87,29 @@ def draw_bb(mask, origin=None):
|
||||
lines.append(f"{r+1} " + " ".join(row))
|
||||
lines.append(" " + " ".join(FILES))
|
||||
lines = "\n".join(lines)
|
||||
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):
|
||||
@@ -123,7 +153,13 @@ class ChessFFI:
|
||||
|
||||
def load_fen(self, board, fen_string):
|
||||
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):
|
||||
buf = (Move * cap)()
|
||||
@@ -131,6 +167,10 @@ class ChessFFI:
|
||||
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):
|
||||
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);
|
||||
@@ -114,4 +114,7 @@ void gen_white_pawn_captures(const struct Board *board, struct Move *moves, int
|
||||
void gen_black_pawn_captures(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);
|
||||
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();
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
// 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';
|
||||
}
|
||||
58
makefile
58
makefile
@@ -2,46 +2,64 @@ CC := gcc
|
||||
CFLAGS := -O3 -fPIC -Wall -Wextra
|
||||
LDFLAGS := -shared
|
||||
|
||||
SRCDIR := engine/src
|
||||
INCDIR := engine/include
|
||||
# ---- paths ----
|
||||
SRCDIRS := engine/src engine/src/ai
|
||||
INCDIRS := engine/include
|
||||
BUILDDIR := build
|
||||
LIBNAME := libchess
|
||||
LIB := $(BUILDDIR)/$(LIBNAME).so
|
||||
|
||||
# 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
|
||||
# ---- 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) ----
|
||||
TESTSRC := $(SRCDIR)/test.c
|
||||
TESTOBJ := $(BUILDDIR)/test.o
|
||||
TESTBIN := $(BUILDDIR)/print_board
|
||||
TESTSRC := engine/src/test.c
|
||||
TESTOBJ := $(BUILDDIR)/engine/src/test.o
|
||||
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)
|
||||
|
||||
$(BUILDDIR):
|
||||
@mkdir -p $(BUILDDIR)
|
||||
# generic object rule: build/<path>/file.o from <path>/file.c
|
||||
$(BUILDDIR)/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@
|
||||
|
||||
$(LIB): $(OBJ) | $(BUILDDIR)
|
||||
$(LIB): $(OBJS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(LDFLAGS) -o $@ $^
|
||||
|
||||
# ---- test exe rules ----
|
||||
test-exe: $(TESTBIN)
|
||||
|
||||
$(TESTOBJ): $(TESTSRC) | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) -I$(INCDIR) -c $< -o $@
|
||||
$(TESTOBJ): $(TESTSRC)
|
||||
@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'
|
||||
|
||||
run-c-test: $(TESTBIN)
|
||||
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:
|
||||
@echo "Cleaning build and Python caches..."
|
||||
@rm -rf $(BUILDDIR)
|
||||
@@ -55,4 +73,4 @@ test:
|
||||
@python3 -m unittest -v; \
|
||||
status=$$?; \
|
||||
$(MAKE) clean; \
|
||||
exit $$status
|
||||
exit $$status
|
||||
|
||||
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