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

62
scripts/evaluation.py Normal file
View File

@@ -0,0 +1,62 @@
import random
import ctypes as C
from binding.python_c_ffi import ChessFFI
from binding.python_c_ffi import Move
class BaseEvaluation:
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
"""
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):
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):
def __init__(self, depth=6, cp_window=20, chess_ffi=None):
super().__init__(chess_ffi)
self.depth = depth
self.window = cp_window
def _same(self, a, b):
return (int(getattr(a, "from")) == int(getattr(b, "from"))
and int(a.to) == int(b.to)
and int(getattr(a, "promo", 0) or 0) == int(getattr(b, "promo", 0) or 0))
def get_best_move(self, board, legal_moves):
if not legal_moves:
raise RuntimeError("No legal moves")
best = Move()
ok = self.chess_ffi._c_ai_find_best_move_with_window(
board,
self.depth,
self.window,
best
)
if ok:
for m in legal_moves:
if self._same(m, best):
return m
return legal_moves[0]

149
scripts/simulation.py Normal file
View File

@@ -0,0 +1,149 @@
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
YMD_HM = "%Y-%m-%d-%H-%M"
MAX_MOVES = 256
DATA_PATH = "./data"
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._load_attack_cache()
self._seed_engine()
random_strat = RandomEval(chess_ffi=self.chess_ffi)
nega_strat = NegaMaxEval(chess_ffi=self.chess_ffi, depth=2, cp_window=30)
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
def run(self, fen):
self.chess_ffi.load_fen(self.board, fen)
self._clear_moves()
plys = 0
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, ending = self._is_game_over(n_legal)
if game_over:
print(ending)
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
move = self.to_uci(best_move)
self.moves.append(move)
if self.board.halfmove_clock >= 100:
print("draw (50-move rule)")
break
for move in self.moves:
# print(move)
pass
print("done")
print(f"winner: {self.side_tag(not self.board.side_to_move)}")
print(len(self.moves))
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 _clear_moves(self):
self.moves = []
def to_uci(self, move):
fr = sq_to_coord(getattr(move, "from"))
to = sq_to_coord(move.to)
promo = getattr(move, "promo", 0) or ""
if promo:
MAP = {
1:"n",
2:"b",
3:"r",
4:"q",
"n":"n",
"b":"b",
"r":"r",
"q":"q"
}
promo = MAP.get(promo, "").lower()
return f"{fr}{to}{promo}"
def _load_attack_cache(self):
self.chess_ffi.init_attack_cache()
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):
ending = "checkmate"
else:
ending = "stalemate"
return True, ending
return False, None
if __name__ == "__main__":
engine = Engine()
fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
engine.run(fen)