diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a55449b..449c37f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -30,6 +30,12 @@ "command": "python -m pip install -r requirements.txt && pytest -q", "group": "build" }, + { + "label": "pytest quick", + "type": "shell", + "command": "python -m pip install -r requirements.txt && pytest -q", + "group": "build" + }, { "label": "pytest quick", "type": "shell", diff --git a/C/lichess_random_engine/Makefile b/C/lichess_random_engine/Makefile index e335ba6..10efc71 100644 --- a/C/lichess_random_engine/Makefile +++ b/C/lichess_random_engine/Makefile @@ -2,9 +2,13 @@ CC := gcc CFLAGS := -O2 -std=c11 -Wall -Wextra -Wno-unused-parameter LDFLAGS := -SRC := main.c +SRC := main.c movegen.c search.c BIN := random_engine +# Perft driver +PERFT_SRC := perft.c movegen.c +PERFT_BIN := perft + .PHONY: all clean rebuild all: $(BIN) @@ -12,7 +16,13 @@ all: $(BIN) $(BIN): $(SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +$(PERFT_BIN): $(PERFT_SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + clean: - rm -f $(BIN) + rm -f $(BIN) $(PERFT_BIN) rebuild: clean all + +.PHONY: perft +perft: $(PERFT_BIN) diff --git a/C/lichess_random_engine/main.c b/C/lichess_random_engine/main.c index 3ab31e0..6e0e464 100644 --- a/C/lichess_random_engine/main.c +++ b/C/lichess_random_engine/main.c @@ -1,1200 +1,188 @@ -// Heuristic engine with optional explanation and analysis output -// Usage: -// random_engine [--seed N] [--fen FEN] [--explain] [--analyze UCI] ... -// Behavior: -// - If --fen is provided, the engine parses the position and computes features per move -// (check, capture value, promotion gain, material delta) directly from the position. -// - If no --fen is provided but a move is annotated as 'uci;key=value;...' the engine -// parses features from annotations. Recognized keys: chk (0/1), c (capture cp), prom (cp gain), -// mat (cp delta), mate (0/1). -// - Otherwise, assigns a pseudo-random score using the seed. -// - Picks the highest-scoring move -// - Default output: prints chosen move -// - With --explain: prints JSON including scores, chosen index, seed, and optional analysis of a provided candidate +// Minimal chess engine CLI: random fallback + alpha-beta search for provided move list +// Contract expected by PYTHON/lichess_bot/engine.py: +// - Usage without explanation: random_engine --fen "" ... +// -> prints the chosen UCI move on stdout +// - With explanation: random_engine --fen "" --explain [--analyze ] ... +// -> prints a compact JSON object containing chosen_move and a simple analyze block +// +// Notes: +// - We don't validate or parse FEN yet; it's accepted for future use. +// - We choose a uniformly random move among the provided UCIs. +// - For "--analyze" the candidate score is a placeholder (0.0) for now. #include #include #include #include -#include -#include -#include - -// --- Minimal chess board utilities for FEN, move application, and attacks --- +#include "movegen.h" +#include "search.h" typedef struct { - char squares[64]; // 0..63 (a1=0, h8=63). Lowercase black, uppercase white; '.' empty - int white_to_move; // 1 if white to move, 0 if black -} Board; + const char *fen; + int explain; + const char *analyze_move; + const char **moves; + int move_count; +} Args; -static int file_of(int idx) { return idx % 8; } -static int rank_of(int idx) { return idx / 8; } -static int idx_from_fr(int f, int r) { return r * 8 + f; } - -static int is_white(char p) { return p >= 'A' && p <= 'Z'; } -static int is_black(char p) { return p >= 'a' && p <= 'z'; } -static int same_color(char a, char b) { return (is_white(a) && is_white(b)) || (is_black(a) && is_black(b)); } - -static int piece_value_cp(char p) { - switch (tolower((unsigned char)p)) { - case 'p': return 100; - case 'n': return 320; - case 'b': return 330; - case 'r': return 500; - case 'q': return 900; - case 'k': return 0; // king's value excluded for material sums - default: return 0; - } +static void print_usage(const char *prog) { + fprintf(stderr, + "Usage: %s --fen '' [--explain] [--analyze ] \n", + prog); } -static int parse_fen(Board *b, const char *fen) { - // Parse piece placement and active color; ignore castling, ep, halfmove, fullmove - memset(b->squares, '.', sizeof(b->squares)); - b->white_to_move = 1; - if (!fen || !*fen) return 0; - // piece placement - int f = 0, r = 7; // start at a8 - const char *p = fen; - while (*p && !(p[0] == ' ')) { - char c = *p++; - if (c == '/') { f = 0; r--; if (r < 0) return 0; continue; } - if (c >= '1' && c <= '8') { f += (c - '0'); if (f > 8) return 0; continue; } - if (isalpha((unsigned char)c)) { - if (f >= 8 || r < 0) return 0; - b->squares[idx_from_fr(f, r)] = c; - f++; - } else { - return 0; - } - } - if (*p == ' ') p++; - // active color - if (*p == 'w') { b->white_to_move = 1; p++; } - else if (*p == 'b') { b->white_to_move = 0; p++; } - // done - return 1; +static int parse_args(int argc, char **argv, Args *out) { + memset(out, 0, sizeof(*out)); + out->moves = NULL; + out->move_count = 0; + + // Collect options regardless of order; every non-option token is a move. + const char **moves = NULL; + int moves_cap = 0; + int moves_len = 0; + + for (int i = 1; i < argc; ++i) { + const char *a = argv[i]; + if (strcmp(a, "--fen") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "--fen requires an argument\n"); + free(moves); + return 0; + } + out->fen = argv[++i]; + continue; + } + if (strcmp(a, "--explain") == 0) { + out->explain = 1; + continue; + } + if (strcmp(a, "--analyze") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "--analyze requires a UCI move\n"); + free(moves); + return 0; + } + out->analyze_move = argv[++i]; + continue; + } + // Otherwise treat as move + if (moves_len >= moves_cap) { + int new_cap = moves_cap == 0 ? 8 : moves_cap * 2; + const char **tmp = (const char**)realloc(moves, (size_t)new_cap * sizeof(const char*)); + if (!tmp) { + fprintf(stderr, "Out of memory\n"); + free(moves); + return 0; + } + moves = tmp; + moves_cap = new_cap; + } + moves[moves_len++] = a; + } + + if (!out->fen) { + fprintf(stderr, "Missing --fen argument\n"); + free(moves); + return 0; + } + + if (moves_len > 0) { + out->moves = moves; // keep ownership until program end + out->move_count = moves_len; + } else { + free(moves); + out->moves = NULL; + out->move_count = 0; + } + return 1; } -static int find_king(const Board *b, int white) { - char k = white ? 'K' : 'k'; - for (int i = 0; i < 64; ++i) if (b->squares[i] == k) return i; - return -1; +static int pick_random_index(int n, const char *fen) { + if (n <= 0) return -1; + // Mix in time and a simple FEN hash for a touch of variety/repeatability. + unsigned long hash = 1469598103934665603ULL; // FNV offset basis + if (fen) { + const unsigned char *p = (const unsigned char*)fen; + while (*p) { + hash ^= (unsigned long)(*p++); + hash *= 1099511628211ULL; // FNV prime + } + } + unsigned long seed = (unsigned long)time(NULL) ^ hash; + srand((unsigned int)(seed ^ (seed >> 32))); + int idx = rand() % n; + return idx; } -static int on_board(int f, int r) { return f >= 0 && f < 8 && r >= 0 && r < 8; } - -static int sq_attacked_by(const Board *b, int target_idx, int by_white) { - int tf = file_of(target_idx), tr = rank_of(target_idx); - // Knights - const int kdf[8] = {1,2, 2,1, -1,-2, -2,-1}; - const int kdr[8] = {2,1, -1,-2, 2,1, -1,-2}; - for (int i = 0; i < 8; ++i) { - int f = tf + kdf[i], r = tr + kdr[i]; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'N' : p == 'n') return 1; - } - } - // King - for (int df = -1; df <= 1; ++df) for (int dr = -1; dr <= 1; ++dr) if (df || dr) { - int f = tf + df, r = tr + dr; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'K' : p == 'k') return 1; - } - } - // Pawns - if (by_white) { - int f1 = tf - 1, r1 = tr - 1; - int f2 = tf + 1, r2 = tr - 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'P') return 1; - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'P') return 1; - } else { - int f1 = tf - 1, r1 = tr + 1; - int f2 = tf + 1, r2 = tr + 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'p') return 1; - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'p') return 1; - } - // Sliding: bishops/queens (diagonals) - const int dsf[4] = {1, 1, -1, -1}; - const int dsr[4] = {1, -1, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + dsf[d], r = tr + dsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'B' || p == 'Q') : (p == 'b' || p == 'q')) return 1; - break; - } - f += dsf[d]; r += dsr[d]; - } - } - // Sliding: rooks/queens (orthogonals) - const int rsf[4] = {1, -1, 0, 0}; - const int rsr[4] = {0, 0, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + rsf[d], r = tr + rsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'R' || p == 'Q') : (p == 'r' || p == 'q')) return 1; - break; - } - f += rsf[d]; r += rsr[d]; - } - } - return 0; -} - -static int count_attackers(const Board *b, int target_idx, int by_white) { - int tf = file_of(target_idx), tr = rank_of(target_idx); - int cnt = 0; - // Knights - const int kdf[8] = {1,2, 2,1, -1,-2, -2,-1}; - const int kdr[8] = {2,1, -1,-2, 2,1, -1,-2}; - for (int i = 0; i < 8; ++i) { - int f = tf + kdf[i], r = tr + kdr[i]; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'N' : p == 'n') cnt++; - } - } - // King - for (int df = -1; df <= 1; ++df) for (int dr = -1; dr <= 1; ++dr) if (df || dr) { - int f = tf + df, r = tr + dr; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'K' : p == 'k') cnt++; - } - } - // Pawns - if (by_white) { - int f1 = tf - 1, r1 = tr - 1; - int f2 = tf + 1, r2 = tr - 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'P') cnt++; - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'P') cnt++; - } else { - int f1 = tf - 1, r1 = tr + 1; - int f2 = tf + 1, r2 = tr + 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'p') cnt++; - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'p') cnt++; - } - // Sliding: bishops/queens (diagonals) - const int dsf[4] = {1, 1, -1, -1}; - const int dsr[4] = {1, -1, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + dsf[d], r = tr + dsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'B' || p == 'Q') : (p == 'b' || p == 'q')) cnt++; - break; - } - f += dsf[d]; r += dsr[d]; - } - } - // Sliding: rooks/queens (orthogonals) - const int rsf[4] = {1, -1, 0, 0}; - const int rsr[4] = {0, 0, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + rsf[d], r = tr + rsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'R' || p == 'Q') : (p == 'r' || p == 'q')) cnt++; - break; - } - f += rsf[d]; r += rsr[d]; - } - } - return cnt; -} - -static int min_attacker_value(const Board *b, int target_idx, int by_white) { - int tf = file_of(target_idx), tr = rank_of(target_idx); - int best = 1e9; - // Knights - const int kdf[8] = {1,2, 2,1, -1,-2, -2,-1}; - const int kdr[8] = {2,1, -1,-2, 2,1, -1,-2}; - for (int i = 0; i < 8; ++i) { - int f = tf + kdf[i], r = tr + kdr[i]; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'N' : p == 'n') { int v = piece_value_cp(p); if (v < best) best = v; } - } - } - // King - for (int df = -1; df <= 1; ++df) for (int dr = -1; dr <= 1; ++dr) if (df || dr) { - int f = tf + df, r = tr + dr; - if (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (by_white ? p == 'K' : p == 'k') { int v = piece_value_cp(p); if (v < best) best = v; } - } - } - // Pawns - if (by_white) { - int f1 = tf - 1, r1 = tr - 1; - int f2 = tf + 1, r2 = tr - 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'P') { int v = 100; if (v < best) best = v; } - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'P') { int v = 100; if (v < best) best = v; } - } else { - int f1 = tf - 1, r1 = tr + 1; - int f2 = tf + 1, r2 = tr + 1; - if (on_board(f1, r1) && b->squares[idx_from_fr(f1, r1)] == 'p') { int v = 100; if (v < best) best = v; } - if (on_board(f2, r2) && b->squares[idx_from_fr(f2, r2)] == 'p') { int v = 100; if (v < best) best = v; } - } - // Sliding: bishops/queens (diagonals) - const int dsf[4] = {1, 1, -1, -1}; - const int dsr[4] = {1, -1, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + dsf[d], r = tr + dsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'B' || p == 'Q') : (p == 'b' || p == 'q')) { int v = piece_value_cp(p); if (v < best) best = v; } - break; - } - f += dsf[d]; r += dsr[d]; - } - } - // Sliding: rooks/queens (orthogonals) - const int rsf[4] = {1, -1, 0, 0}; - const int rsr[4] = {0, 0, 1, -1}; - for (int d = 0; d < 4; ++d) { - int f = tf + rsf[d], r = tr + rsr[d]; - while (on_board(f, r)) { - char p = b->squares[idx_from_fr(f, r)]; - if (p != '.') { - if (by_white ? (p == 'R' || p == 'Q') : (p == 'r' || p == 'q')) { int v = piece_value_cp(p); if (v < best) best = v; } - break; - } - f += rsf[d]; r += rsr[d]; - } - } - if (best == (int)1e9) return 0; - return best; -} - -static int material_cp(const Board *b) { - int w = 0, bl = 0; - for (int i = 0; i < 64; ++i) { - char p = b->squares[i]; - if (p == '.') continue; - int v = piece_value_cp(p); - if (is_white(p)) w += v; else bl += v; - } - return w - bl; // positive if white ahead -} - -static int parse_uci_move(const char *uci, int *from, int *to, char *prom) { - // uci like e2e4, e7e8q - if (!uci || strlen(uci) < 4) return 0; - int f1 = uci[0] - 'a'; - int r1 = uci[1] - '1'; - int f2 = uci[2] - 'a'; - int r2 = uci[3] - '1'; - if (!on_board(f1, r1) || !on_board(f2, r2)) return 0; - *from = idx_from_fr(f1, r1); - *to = idx_from_fr(f2, r2); - *prom = 0; - if (uci[4]) { - *prom = uci[4]; - } - return 1; -} - -static void apply_move(const Board *in, const char *uci, Board *out, int *cap_cp, int *prom_gain_cp) { - *out = *in; // shallow copy - *cap_cp = 0; - *prom_gain_cp = 0; - int from, to; char prom; - if (!parse_uci_move(uci, &from, &to, &prom)) return; - char mover = out->squares[from]; - char captured = out->squares[to]; - if (captured != '.') *cap_cp = piece_value_cp(captured); - // move piece - out->squares[to] = mover; - out->squares[from] = '.'; - // handle promotion - if (prom) { - int is_w = is_white(mover); - char p = (char)tolower((unsigned char)prom); - char prom_piece = p == 'q' ? (is_w ? 'Q' : 'q') : p == 'r' ? (is_w ? 'R' : 'r') : p == 'b' ? (is_w ? 'B' : 'b') : (is_w ? 'N' : 'n'); - int gain = piece_value_cp(prom_piece) - piece_value_cp(is_w ? 'P' : 'p'); - *prom_gain_cp = gain; - out->squares[to] = prom_piece; - } - // toggle side to move - out->white_to_move = !in->white_to_move; -} - -static unsigned int parse_seed_or_default(int *pargc, char ***pargv) { - unsigned int seed = (unsigned int)time(NULL) ^ (unsigned int)getpid(); - int argc = *pargc; - char **argv = *pargv; - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--seed") == 0 && i + 1 < argc) { - seed = (unsigned int)strtoul(argv[i + 1], NULL, 10); - // remove the two args - for (int j = i; j + 2 < argc; ++j) argv[j] = argv[j + 2]; - *pargc -= 2; - return seed; - } - } - return seed; -} - -typedef struct { - const char *arg_raw; // original argument string - char uci[16]; // extracted UCI (up to 7-8 chars normally) - int has_anno; // whether annotations were present - // parsed features - int chk; // 0/1 - int mate; // 0/1 - int in_check; // 0/1: side to move is currently in check (pre-move) - double cap_cp; // capture centipawns - double prom_cp; // promotion centipawns - double mat_cp; // material delta centipawns - // attackers/defenders heuristic after the move lands on destination - double opp_min_att_cp; // opponent's least valuable attacker on destination (after move) - double us_min_att_cp; // our least valuable attacker on destination (after move) - double piece_cp; // our moved piece's value after move (post-promotion) - double see_cp; // simple SEE: cap_cp - opp_min_att_cp (captures only) - double risk_cp; // if non-capture and square is attacked by opp and not defended by us, risk ~= min(piece_cp, opp_min_att_cp) - // king attack/mobility features - double atk_opp_king; // number of our attackers to opponent's king square after move - double opp_king_mob; // opponent king escape squares after move (lower is better) - // threat features - double threat_q; // after move, our side attacks enemy queen square - double threat_r; // after move, our side attacks enemy rook square (any) - double prox_king; // destination square adjacent to enemy king - double score; // computed score - // check characterization - int checker_is_slider; // 1 if checking piece is rook/bishop/queen - int line_check_blockable; // 1 if sliding check has at least one interposing square (thus blockable) - // opponent threats on our heavy pieces after our move - double opp_threat_our_q; // opponent attacks our queen square (after move) - double opp_threat_our_r; // opponent attacks our rook squares (sum over rooks, 0.5 each) - // our defenders of our heavy pieces (to scale penalties) - double our_q_def; // number of our attackers defending our queen square - double our_r_def; // aggregated (0.5 per rook with at least one defender) - // destination geometry features - double to_central; // 0..1 centrality of destination square - double rook_on_7th; // 1 if rook lands on 7th (white) or 2nd (black) rank - // baseline threats before move (from original position); helps evaluate reductions - double base_opp_threat_our_q; // 1.0 if our queen is attacked in the initial position - double base_opp_threat_our_r; // aggregated 0.5 per rook attacked initially -} MoveInfo; - -static void parse_move_spec(const char *spec, MoveInfo *mi) { - // Copy UCI up to ';' or end - mi->arg_raw = spec; - mi->uci[0] = '\0'; - mi->has_anno = 0; - mi->chk = 0; - mi->mate = 0; - mi->in_check = 0; - mi->cap_cp = 0.0; - mi->prom_cp = 0.0; - mi->mat_cp = 0.0; - mi->opp_min_att_cp = 0.0; - mi->us_min_att_cp = 0.0; - mi->piece_cp = 0.0; - mi->see_cp = 0.0; - mi->risk_cp = 0.0; - mi->atk_opp_king = 0.0; - mi->opp_king_mob = 0.0; - mi->threat_q = 0.0; - mi->threat_r = 0.0; - mi->prox_king = 0.0; - mi->score = 0.0; - mi->checker_is_slider = 0; - mi->line_check_blockable = 0; - mi->opp_threat_our_q = 0.0; - mi->opp_threat_our_r = 0.0; - mi->our_q_def = 0.0; - mi->our_r_def = 0.0; - mi->to_central = 0.0; - mi->rook_on_7th = 0.0; - mi->base_opp_threat_our_q = 0.0; - mi->base_opp_threat_our_r = 0.0; - - const char *semi = strchr(spec, ';'); - size_t uci_len = semi ? (size_t)(semi - spec) : strlen(spec); - if (uci_len >= sizeof(mi->uci)) uci_len = sizeof(mi->uci) - 1; - memcpy(mi->uci, spec, uci_len); - mi->uci[uci_len] = '\0'; - - if (!semi) return; - mi->has_anno = 1; - const char *p = semi + 1; - while (*p) { - // key=value; segments - const char *kv_end = strchr(p, ';'); - size_t len = kv_end ? (size_t)(kv_end - p) : strlen(p); - if (len > 0) { - // Parse known keys: chk, mate, c, prom, mat - if (strncmp(p, "chk=", 4) == 0) { - mi->chk = atoi(p + 4); - } else if (strncmp(p, "mate=", 5) == 0) { - mi->mate = atoi(p + 5); - } else if (strncmp(p, "c=", 2) == 0) { - mi->cap_cp = atof(p + 2); - } else if (strncmp(p, "prom=", 6) == 0) { - mi->prom_cp = atof(p + 6); - } else if (strncmp(p, "mat=", 4) == 0) { - mi->mat_cp = atof(p + 4); - } - } - if (!kv_end) break; - p = kv_end + 1; - } -} - -static double heuristic_score(const MoveInfo *mi, unsigned int seed_state) { - // Weighted score from features; add tiny noise from seed to break ties - double s = 0.0; - if (mi->mate) s += 100000.0; // winning immediately trumps all - // Checks: scale by pressure; devalue "empty" checks with no material promise - if (mi->chk) { - double chk_bonus = 250.0 + 80.0 * mi->atk_opp_king - 60.0 * mi->opp_king_mob; - // if checker cannot be captured cheaply - if (mi->opp_min_att_cp <= 0.0) { - // For queen checks, don't over-reward being "safe"; it's still a queen in play - chk_bonus += (mi->piece_cp >= 850.0) ? 80.0 : 500.0; - } else if (mi->opp_min_att_cp >= mi->piece_cp - 1e-6) { - // Only expensive capture; scale down for queen - chk_bonus += (mi->piece_cp >= 850.0) ? 50.0 : 300.0; - } - // if our defender is cheaper than their attacker, exchange favors us - if (mi->us_min_att_cp > 0.0 && mi->opp_min_att_cp > 0.0 && mi->us_min_att_cp < mi->opp_min_att_cp) chk_bonus += 150.0; - // strongly devalue queen checks that only pick up a pawn (common blunder bait) - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 100.0 && mi->prom_cp <= 0.0) { - // if king still has escapes, this is usually just a bait check - if (mi->opp_king_mob > 0.0) chk_bonus *= 0.10; else chk_bonus *= 0.25; - } - // big bonus when the check leaves the opponent king with no legal escapes - if (mi->opp_king_mob <= 0.0) { - // Zero king mobility is a strong tactical motif even if the check can be blocked - if (mi->checker_is_slider && mi->line_check_blockable) { - chk_bonus += 800.0; - } else { - chk_bonus += 1500.0; - } - } - // Checking captures are especially forcing, except when it's a queen snatching a pawn - if (mi->cap_cp > 0.0) { - double add = 400.0; - if (mi->piece_cp >= 850.0) { - // Queen check-captures: big when taking heavy pieces, modest otherwise - if (mi->cap_cp >= 500.0) add = 900.0; else add = (mi->cap_cp <= 120.0 && mi->opp_king_mob > 0.0) ? 30.0 : 220.0; - } - chk_bonus += add; - } - // If it's a rook delivering a light check and king has many escapes, devalue sharply, - // especially when the destination has no friendly cover. - if (mi->piece_cp == 500.0 && mi->cap_cp <= 0.0 && mi->opp_king_mob >= 3.0) { - // Centralizing rook checks are less "empty" tactically; dampen less when moving to a central file - int is_central = (mi->to_central >= 0.70); - if (mi->us_min_att_cp <= 0.0 && mi->our_r_def <= 0.0) chk_bonus *= is_central ? 0.65 : 0.35; else chk_bonus *= is_central ? 0.8 : 0.5; - } - if (mi->opp_king_mob > 0.0 && mi->cap_cp <= 0.0 && mi->see_cp <= 0.0 && mi->mat_cp <= 0.0) { - // For centralizing rook checks, keep more of the check bonus - if (mi->chk && mi->piece_cp == 500.0 && mi->to_central >= 0.70) chk_bonus *= 0.6; - else if (mi->chk && mi->piece_cp >= 850.0) { - // Queen empty checks are rarely best when king has outs - double damp = (mi->opp_threat_our_q > 0.0 ? 0.05 : 0.15); - if (mi->opp_king_mob <= 1.0) damp = 0.35; // pressing checks with few escapes deserve more credit - chk_bonus *= damp; - } else chk_bonus *= 0.3; - } - s += chk_bonus; - } - s += 1.5 * mi->cap_cp; // value captures strongly - // prefer winning exchanges where the capturing piece is cheaper than the captured value - if (mi->cap_cp > 0.0) { - if (mi->piece_cp >= 850.0) { - // Queen captures: be cautious; usually lead to heavy trades - s += 0.35 * (mi->cap_cp - mi->piece_cp); - // Tactical exception: queen checking capture of a rook with only a minor as cheapest recapture - int tactical_qr_check = (mi->cap_cp >= 500.0 && mi->chk && ((mi->opp_min_att_cp == 0.0) || (mi->opp_min_att_cp > 0.0 && mi->opp_min_att_cp <= 350.0))); - if (!tactical_qr_check) { - // Prefer safe queen captures; discourage only the risky ones - if (mi->see_cp >= 0.0) { - // Reward safe queen captures more when not taking a queen; queen-vs-queen trades are volatile - s += (mi->cap_cp >= 850.0 ? 120.0 : 260.0); - } else { - s -= 180.0; - } - if (mi->chk <= 0 && mi->see_cp < 0.0) { - s -= 150.0; // additional nudge: non-checking queen captures are less forcing when SEE bad - } - // If our queen capture allows immediate queen recapture (opp attacker min is queen), punish - if (mi->opp_min_att_cp >= 850.0) s -= 250.0; - // If SEE is substantially negative after a queen capture, punish further - if (mi->see_cp < -100.0) s -= 350.0; - // Strongly penalize non-check queen pawn snatches that don't create pressure - if (mi->cap_cp <= 120.0 && mi->chk <= 0 && mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->opp_king_mob > 0.0) { - s -= 400.0; - } - // Under pressure (in check or heavy pieces attacked), allow queen minor recaptures more - if (mi->cap_cp >= 300.0 && mi->cap_cp <= 350.0 && (mi->in_check || mi->base_opp_threat_our_q > 0.0 || mi->base_opp_threat_our_r > 0.0)) { - s += 350.0; - } - // Penalize queen-vs-queen captures unless overwhelmingly good by SEE and follow-up - if (mi->cap_cp >= 850.0) { - s -= 300.0; - if (mi->opp_min_att_cp >= 480.0 && mi->opp_min_att_cp <= 520.0) s -= 200.0; // rook recapture looming - } - } else { - // strong incentive for forcing deflection/clearance captures (QxR+ with minor recapture) - s += 950.0; - } - if (mi->opp_min_att_cp >= 500.0) s -= 200.0; // likely immediate recapture by heavy piece - if (mi->us_min_att_cp <= 0.0 && mi->opp_min_att_cp > 0.0) s -= 600.0; // no friendly cover on destination - // Non-check heavy recapture by queen, but safe (SEE >= 0): allow it when necessary - if (mi->cap_cp >= 480.0 && mi->cap_cp <= 520.0 && mi->chk <= 0 && mi->see_cp >= 0.0) { - s += 500.0; - } - // If both our queen and a rook will remain under fire after a queen capture and we have no cover, avoid it - if (mi->opp_threat_our_q > 0.0 && mi->opp_threat_our_r > 0.0 && mi->us_min_att_cp <= 0.0) { - s -= 500.0; - } - // Even without giving check: safe queen capture of a rook with only a minor as cheapest recapture is often winning - if (mi->cap_cp >= 500.0 && mi->chk <= 0 && mi->see_cp >= 0.0 && mi->opp_min_att_cp > 0.0 && mi->opp_min_att_cp <= 350.0) { - s += 850.0; - } - // Central safe queen capture of a minor often consolidates initiative - if (mi->cap_cp >= 280.0 && mi->cap_cp <= 350.0 && mi->see_cp >= 0.0 && mi->to_central >= 0.50) { - s += 100.0; - } - } else { - double exch = mi->cap_cp - mi->piece_cp; - double exch_w = (mi->piece_cp <= 120.0) ? 1.0 : 3.5; // further dampen pawn capture bias - // If a heavy piece safely captures a pawn (no opponent attackers), dramatically soften the - // exchange penalty; these "tidying up" captures are often correct in endgames. - if (mi->cap_cp <= 120.0 && mi->opp_min_att_cp <= 0.0 && mi->piece_cp >= 400.0) { - exch_w = 1.0; - } - s += exch_w * exch; - // Extra nudge: safe rook pawn pickup (cleaning up a passer) is usually good - if (mi->piece_cp == 500.0 && mi->cap_cp <= 120.0 && mi->opp_min_att_cp <= 0.0) { - s += 300.0; - } - // Prefer minor taking heavy piece, or rook taking queen - if ((mi->piece_cp <= 350.0 && mi->cap_cp >= 500.0) || (mi->piece_cp == 500.0 && mi->cap_cp >= 900.0)) { - s += 300.0; - } - // Strongly prefer minor piece (B/N) capturing a heavy piece - if (mi->cap_cp >= 500.0 && mi->piece_cp <= 350.0) s += 900.0; - // No generic heavy-vs-heavy penalty; handled by context-specific terms - // Discourage rook-vs-rook captures that land under pawn attack without cover - if (mi->piece_cp == 500.0 && mi->cap_cp >= 500.0 && mi->opp_min_att_cp > 0.0 && mi->opp_min_att_cp <= 100.0 && mi->us_min_att_cp <= 0.0) { - s -= 700.0; - } - } - } - s += 2.0 * mi->prom_cp; // promotions are very strong - // micro-bonus for central pawn captures improving structure/space - // stronger bonus for central pawn captures improving structure/space - if (mi->cap_cp > 0.0 && mi->piece_cp <= 120.0) s += 360.0 * mi->to_central; - // material swing, but discount when the gain is from a capture that moves into enemy fire - double mat_term = 1.5 * mi->mat_cp; - if (mi->cap_cp > 0.0 && mi->opp_min_att_cp > 0.0) { - // if our piece on destination is at least as expensive as their cheapest attacker, discount, - // but keep more of the material when the exchange is clearly favorable (SEE positive or big value gap) - if (mi->piece_cp >= mi->opp_min_att_cp - 1e-6) { - double discount = 0.35; - if (mi->piece_cp < 850.0) { - double gap = mi->cap_cp - mi->piece_cp; // how favorable the capture is by piece type - if (gap >= 150.0) discount = 0.8; // minor takes rook/queen -> keep most of the material - if (mi->see_cp >= 200.0) discount += 0.1; // further relax when SEE agrees - if (discount > 0.9) discount = 0.9; - } - mat_term *= discount; - } - // queen-specific extra discount always applies for risky queen plant - if (mi->piece_cp >= 850.0) mat_term *= 0.35; - } - s += mat_term; - s += 0.15 * mi->see_cp; // prefer profitable captures after recapture (more tempered) - s -= 1.0 * mi->risk_cp; // avoid walking into obvious captures - s += 40.0 * mi->atk_opp_king; // general king pressure - // Penalize opponent king mobility less when the move gives check (forcing) - double mob_w = mi->chk ? 15.0 : 40.0; - s -= mob_w * mi->opp_king_mob; // reduce opponent king mobility (moderate impact) - // Encourage rook on 7th/2nd rank pressure - if (mi->rook_on_7th > 0.0) s += 120.0; - // Heuristic: encourage rook horizontal alignment towards enemy king wing (files f/g/h) - // Simple proxy: if rook moves to file f/g/h on 7th/2nd, add small bonus - // (This tends to favor Rf7 over Rg7 when king safety/recapture differ subtly.) - // Compute destination file index from to_central derivation context is not available here, so we approximate via piece value and rook_on_7th only. - // Note: to refine, we would carry destination file explicitly; skipped to keep changes small. - // destination safety: even if defended, prefer squares where the cheapest opponent attacker - // is not much cheaper than our defender or our moved piece - if (mi->opp_min_att_cp > 0.0) { - double ref = mi->piece_cp; - if (mi->us_min_att_cp > 0.0 && mi->us_min_att_cp < ref) ref = mi->us_min_att_cp; - double slack = ref - mi->opp_min_att_cp; // positive means they can start a favorable exchange - if (slack > 0.0) s -= 0.3 * slack; - } - // Discourage low-value recaptures that hand the initiative back (pawn taking our minor while enabling rook threats) - if (mi->cap_cp >= 300.0 && mi->piece_cp <= 120.0 && mi->atk_opp_king <= 0.0 && mi->threat_r > 0.0) { - s -= 600.0; - } - // Defensive interposition: encourage rook blocks where recapture favors us (e.g., ...Rd8) - // Only rooks should receive this large bonus; avoid giving it to queen interpositions. - if (mi->cap_cp <= 0.0 && mi->piece_cp == 500.0 && mi->opp_min_att_cp >= 850.0 && mi->us_min_att_cp > 0.0 && mi->us_min_att_cp <= 350.0) { - s += 1200.0; - } - // When in check, strongly prefer blocking with a minor piece (interposition) - // Heuristic baseline: any non-capture minor move while in check - if (mi->in_check && mi->cap_cp <= 0.0 && mi->piece_cp > 0.0 && mi->piece_cp <= 350.0) { - s += 900.0; - } - // Additional boost when the interposition square is covered by us and their cheapest attacker is heavy - if (mi->in_check && mi->cap_cp <= 0.0 && mi->piece_cp > 0.0 && mi->piece_cp <= 350.0 && mi->opp_min_att_cp >= 500.0 && mi->us_min_att_cp > 0.0) { - s += 600.0; - } - // when currently in check, avoid queen captures unless overwhelmingly good - if (mi->in_check && mi->cap_cp > 0.0 && mi->piece_cp >= 850.0) { - s -= 800.0; - } - // While in check, discourage quiet queen interpositions that don't add forcing value - if (mi->in_check && mi->cap_cp <= 0.0 && mi->piece_cp >= 850.0 && !mi->chk) { - s -= 700.0; - } - // And even queen cross-checks should be secondary to solid minor interpositions - if (mi->in_check && mi->cap_cp <= 0.0 && mi->piece_cp >= 850.0 && mi->chk) { - s -= 200.0; - } - { - // If our queen ends up under attack, reduce credit for creating threats with the queen - double q_threat_w = 80.0; - if (mi->opp_threat_our_q > 0.0) q_threat_w *= 0.7; // still valuable when coordinated - s += q_threat_w * mi->threat_q; // direct queen threat - double r_threat_w = (mi->piece_cp >= 850.0) ? 400.0 : 180.0; - s += r_threat_w * mi->threat_r; // rook threat (encourage pressure like Qd6 hitting Rd8) - // Synergy: queen move creating simultaneous threats on queen and rook - double synergy = (mi->piece_cp >= 850.0 && mi->threat_q > 0.0 && mi->threat_r > 0.0) ? 350.0 : 0.0; - if (mi->opp_threat_our_q > 0.0) synergy *= 0.3; - s += synergy; - } - // Quiet minor move that increases queen safety (defends our queen) gets a consolidation bonus - if (mi->cap_cp <= 0.0 && mi->piece_cp > 0.0 && mi->piece_cp <= 350.0) { - // Case A: we add defenders while queen remains attacked - if (mi->our_q_def > 0.0 && mi->opp_threat_our_q > 0.0) { - s += 900.0; - } - // Case B: baseline our queen was attacked, and this move neutralizes that attack entirely - if (mi->base_opp_threat_our_q > 0.0 && mi->opp_threat_our_q <= 0.0) { - s += 1000.0; - } - } - // Extra encouragement: rook on 7th/2nd that also creates rook threats - if (mi->piece_cp == 500.0 && mi->rook_on_7th > 0.0 && mi->threat_r > 0.0) { - s += 80.0; - } - // Explicit penalty: empty rook check with many king escapes and no added threats - if (mi->piece_cp == 500.0 && mi->chk && mi->cap_cp <= 0.0 && mi->opp_king_mob >= 3.0 && mi->threat_q <= 0.0 && mi->threat_r <= 0.0) { - if (mi->to_central >= 0.70) { - // Waive harsh penalty when the rook check lands centrally; treat as useful improvement - s -= 20.0; - } else { - s -= 180.0; - if (mi->our_r_def <= 0.0) s -= 60.0; // even worse if the checking rook is undefended - } - } - // Penalize leaving our heavy pieces hanging after the move; scale by lack of defenders - if (mi->opp_threat_our_q > 0.0) { - double def_scale_q = (mi->our_q_def > 0.0) ? 0.5 : 1.0; - // If our move is a defensive interposition with favorable recapture, further soften queen-under-attack penalty - if (mi->cap_cp <= 0.0 && mi->piece_cp >= 500.0 && mi->opp_min_att_cp >= 850.0 && mi->us_min_att_cp > 0.0 && mi->us_min_att_cp <= 350.0) { - def_scale_q *= 0.6; - } - // Mutual queen attack but we create extra threats (like Qd6 hitting their rook) - if (mi->piece_cp >= 850.0 && mi->threat_q > 0.0 && mi->threat_r > 0.0) { - def_scale_q *= 0.2; // much softer; our queen is active, not hanging - s += 60.0; // encourage multipurpose standoff - } - s -= def_scale_q * 260.0; - // Prefer defended rook landing squares on quiet moves - if (mi->cap_cp <= 0.0 && mi->piece_cp == 500.0 && mi->our_r_def > 0.0) { - s += 120.0 * mi->our_r_def; // up to +60 per defended rook - } - } - if (mi->opp_threat_our_r > 0.0) { - double def_scale_r = (mi->our_r_def > 0.0) ? 0.6 : 1.0; - // If this rook move is a defensive interposition with favorable recapture, waive the penalty - if (mi->cap_cp <= 0.0 && mi->piece_cp >= 500.0 && mi->opp_min_att_cp >= 850.0 && mi->us_min_att_cp > 0.0 && mi->us_min_att_cp <= 350.0) { - def_scale_r = 0.0; - } - // If our rook is on the 7th/2nd rank and we have defenders, soften exposure penalty a lot - if (mi->rook_on_7th > 0.0 && mi->our_r_def > 0.0) { - def_scale_r *= 0.3; - } - // If this rook move safely recaptures a minor (SEE not terrible), soften exposure penalty - if (mi->piece_cp == 500.0 && mi->cap_cp >= 300.0 && mi->cap_cp <= 350.0 && mi->see_cp >= -50.0) { - def_scale_r *= 0.35; - } - // If we just made a favorable minor-vs-heavy capture with good SEE, don't over-penalize residual rook exposure - if (mi->cap_cp >= 500.0 && mi->piece_cp <= 350.0 && mi->see_cp >= 200.0) { - def_scale_r *= 0.1; - } - s -= def_scale_r * (700.0 * mi->opp_threat_our_r); - } - // Penalize queen sidesteps that leave our rooks under fire and don't gain material - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && mi->opp_threat_our_r > 0.0 && mi->mat_cp <= 0.0) { - // penalize only truly passive queen sidesteps that don't create new pressure - if (mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->atk_opp_king <= 0.0 && mi->prox_king <= 0.0) { - s -= 400.0; - } - } - // If our rooks were under fire initially, reward queen interpositions that fully neutralize it, especially centrally - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && mi->base_opp_threat_our_r >= 0.5 && mi->opp_threat_our_r <= 0.0) { - double gain = 600.0; - if (mi->to_central >= 0.5) gain += 220.0 * mi->to_central; - s += gain; - } - // Conversely, penalize queen sidesteps that leave rook threats unresolved when they were present - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && mi->base_opp_threat_our_r > 0.0 && mi->opp_threat_our_r >= mi->base_opp_threat_our_r - 1e-9) { - if (mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->atk_opp_king <= 0.0) s -= 600.0; - } - // Queen central consolidation: reward queen quiet moves that neutralize rook threats while centralizing - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && mi->opp_threat_our_r <= 0.0) { - if (mi->to_central >= 0.50) s += 220.0 * mi->to_central; - } - // Prefer central queen consolidations when our queen or rooks are under fire - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && (mi->opp_threat_our_q > 0.0 || mi->opp_threat_our_r > 0.0) && mi->to_central >= 0.70) { - s += 200.0; - } - // Small reward for neutralizing threats on our rooks entirely - if (mi->opp_threat_our_r <= 0.0 && mi->piece_cp >= 500.0) s += 80.0; - // Rook interposition/consolidation: when our rooks were under fire in the baseline, reward quiet rook moves - // that reduce or eliminate that threat. Conversely, penalize sidesteps that don't help. - if (mi->piece_cp == 500.0 && mi->cap_cp <= 0.0 && mi->base_opp_threat_our_r > 0.0) { - // Full neutralization - if (mi->opp_threat_our_r <= 0.0) { - double gain = 900.0; - // extra when the rook lands on a square we defend - gain += 200.0 * (mi->our_r_def > 0.0 ? mi->our_r_def : 0.0); - // central interposition tends to be better - gain += 120.0 * mi->to_central; - s += gain; - } else if (mi->opp_threat_our_r < mi->base_opp_threat_our_r - 1e-9) { - // Partial reduction - s += 500.0; - } else { - // No improvement: discourage ineffective rook quiets under pressure - s -= 350.0; - } - } - // Overloaded queen: moving queen while both our queen and a rook are under attack - if (mi->piece_cp >= 850.0 && mi->cap_cp <= 0.0 && mi->opp_threat_our_q > 0.0 && mi->opp_threat_our_r > 0.0) { - s -= 500.0; - } - s += 20.0 * mi->prox_king; // piece near king often creates tactics - // Central rook checks are a bit more valuable than edge checks - if (mi->chk && mi->piece_cp == 500.0) { - double cb = 160.0 * mi->to_central; - // But if it's an empty check and the king has many escapes, dampen the centrality bonus - if (mi->opp_king_mob >= 3.0 && mi->cap_cp <= 0.0 && mi->threat_q <= 0.0 && mi->threat_r <= 0.0) cb *= 0.75; - s += cb; - } - // light discouragements/encouragements for piece activity - if (mi->cap_cp <= 0.0) { - // discourage quiet king shuffles - if (mi->piece_cp == 0.0) s -= 50.0; - // discourage quiet pawn pushes in tactical positions - if (mi->piece_cp == 100.0) s -= 30.0; - // discourage quiet queen moves unless they create threats/pressure - if (mi->piece_cp >= 850.0 && !mi->chk) { - if (mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->atk_opp_king <= 0.0 && mi->prox_king <= 0.0) { - // Exempt centralizing queen moves that improve her safety - if (!(mi->our_q_def > 0.0 && mi->opp_threat_our_q > 0.0 && mi->to_central >= 0.70)) { - s -= 200.0; - } - } - // Reward queen centralization that also improves queen safety - if (mi->our_q_def > 0.0 && mi->opp_threat_our_q > 0.0) { - s += 160.0 + 160.0 * mi->to_central; - } - // Penalize queen quiets that keep her under attack without adding defenders - if (mi->opp_threat_our_q > 0.0 && mi->our_q_def <= 0.0) { - s -= 400.0; - } - // Additional penalty if queen is still under attack and the move didn't create threats - if (mi->opp_threat_our_q > 0.0 && mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->atk_opp_king <= 0.0) { - s -= 350.0; - } - // Central queen consolidation when under heavy-piece pressure (baseline), even if threats remain - if ((mi->base_opp_threat_our_q > 0.0 || mi->base_opp_threat_our_r > 0.0) && mi->to_central >= 0.5) { - s += 200.0 * mi->to_central; - } - // Penalize drifting to the rim without creating new pressure - if (mi->to_central < 0.40 && mi->threat_q <= 0.0 && mi->threat_r <= 0.0 && mi->atk_opp_king <= 0.0) { - s -= 350.0; - } - } - } - // tiny deterministic jitter from seed - double jitter = (double)(seed_state % 1000) / 1000000.0; // up to 0.001 - return s + jitter; +static int find_best_move_from_ucis(const char **ucis, int n_ucis, const char *fen, int depth, int *out_index){ + Position pos; + if (!parse_fen(&pos, fen)) return 0; + // Convert UCI list into legal moves vetted by our generator, but preserve provided order as fallback + Move legal[256]; int map_idx[256]; int L=0; + for (int i=0;i best_score){ best_score = score; best_idx = i; bf = legal[i].from; bt = legal[i].to; } + } + // Map best move back to index in original ucis list using map_idx + if (out_index) *out_index = map_idx[best_idx]; + return 1; } int main(int argc, char **argv) { - if (argc <= 1) { - fprintf(stderr, "usage: %s [--seed N] [--explain] [--analyze UCI] ...\n", argv[0]); - return 1; - } + Args args; + if (!parse_args(argc, argv, &args)) { + print_usage(argv[0]); + return 2; + } - // Extract seed first (if any) - unsigned int seed = parse_seed_or_default(&argc, &argv); - srand(seed); + if (args.move_count <= 0) { + // No legal moves provided; output nothing to keep contract simple. + if (args.explain) { + // Still return a valid JSON object for callers expecting it. + printf("{\"chosen_index\":-1,\"chosen_move\":\"\",\"analyze\":{\"candidate_move\":\"%s\",\"candidate_score\":0.0}}\n", + args.analyze_move ? args.analyze_move : ""); + } + return 0; + } - // Parse flags --explain and --analyze UCI - int explain = 0; - const char *analyze_uci = NULL; - const char *fen = NULL; - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--explain") == 0) { - explain = 1; - for (int j = i; j + 1 < argc; ++j) argv[j] = argv[j + 1]; - argc -= 1; - i -= 1; - } else if (strcmp(argv[i], "--fen") == 0 && i + 1 < argc) { - fen = argv[i + 1]; - for (int j = i; j + 2 < argc; ++j) argv[j] = argv[j + 2]; - argc -= 2; - i -= 1; - } else if (strcmp(argv[i], "--analyze") == 0 && i + 1 < argc) { - analyze_uci = argv[i + 1]; - for (int j = i; j + 2 < argc; ++j) argv[j] = argv[j + 2]; - argc -= 2; - i -= 1; - } - } + // If we have a FEN and move list, run a shallow alpha-beta to choose among provided moves. + int chosen_idx = -1; + if (args.fen && args.move_count>0){ + if (!find_best_move_from_ucis(args.moves, args.move_count, args.fen, 3, &chosen_idx)){ + chosen_idx = pick_random_index(args.move_count, args.fen); + } + } else { + chosen_idx = pick_random_index(args.move_count, args.fen); + } + if (chosen_idx < 0 || chosen_idx >= args.move_count) { + fprintf(stderr, "Internal error picking move index\n"); + return 1; + } + const char *chosen = args.moves[chosen_idx]; - if (argc <= 1) { - fprintf(stderr, "no moves provided\n"); - return 1; - } + if (!args.explain) { + printf("%s\n", chosen); + return 0; + } - // Remaining args are moves - int n = argc - 1; - char **moves = &argv[1]; + // Minimal JSON explanation compatible with engine.py's parser + // Fields consumed by Python wrapper: + // - chosen_move (string) + // - chosen_index (int) + // - analyze.candidate_score (number) [optional but provided] + // Additionally include analyze.candidate_move for easier debugging. + const char *cand = args.analyze_move ? args.analyze_move : ""; + double cand_score = 0.0; // placeholder; real eval will come later - // Parse move specs - MoveInfo *info = (MoveInfo *)malloc(sizeof(MoveInfo) * (size_t)n); - if (!info) { - fprintf(stderr, "alloc failed\n"); - return 1; - } - Board board; - int have_pos = 0; - if (fen) { - if (!parse_fen(&board, fen)) { - fprintf(stderr, "invalid FEN\n"); - free(info); - return 1; - } - have_pos = 1; - } - int base_mat = 0; - if (have_pos) base_mat = material_cp(&board); - - // Precompute if current side is in check - int side_in_check = 0; - if (have_pos) { - int my_king = find_king(&board, board.white_to_move); - if (my_king >= 0) side_in_check = sq_attacked_by(&board, my_king, !board.white_to_move); - } - - // Baseline threats on our heavy pieces in the current position (before any move) - double base_opp_threat_our_q = 0.0; - double base_opp_threat_our_r = 0.0; - if (have_pos) { - int our_is_white0 = board.white_to_move; // side to move is our side before applying move - int opp_is_white0 = !our_is_white0; - for (int sq = 0; sq < 64; ++sq) { - char p0 = board.squares[sq]; - if (p0 == '.') continue; - if (our_is_white0 ? is_white(p0) : is_black(p0)) { - int tl0 = (int)tolower((unsigned char)p0); - if (tl0 == 'q') { - if (count_attackers(&board, sq, opp_is_white0) > 0) base_opp_threat_our_q = 1.0; - } else if (tl0 == 'r') { - if (count_attackers(&board, sq, opp_is_white0) > 0) base_opp_threat_our_r += 0.5; - } - } - } - } - - for (int i = 0; i < n; ++i) { - parse_move_spec(moves[i], &info[i]); - if (have_pos) { - // derive features from position by applying the move - Board after = board; - int cap_cp = 0, prom_gain = 0; - apply_move(&board, info[i].uci, &after, &cap_cp, &prom_gain); - int mat_after = material_cp(&after); - int mat_raw = mat_after - base_mat; // positive if white improved - int mat_signed = board.white_to_move ? mat_raw : -mat_raw; // positive if mover improved - // set features - info[i].in_check = side_in_check; - info[i].cap_cp = (double)cap_cp; - info[i].prom_cp = (double)prom_gain; - info[i].mat_cp = (double)mat_signed; - // attacker/defender stats on destination square - int from, to; char pr; - if (parse_uci_move(info[i].uci, &from, &to, &pr)) { - char landed = after.squares[to]; - info[i].piece_cp = (double)piece_value_cp(landed); - int opp_is_white = after.white_to_move; // after our move, it's opponent to move - int us_is_white = !after.white_to_move; - int opp_min = min_attacker_value(&after, to, opp_is_white); - int us_min = min_attacker_value(&after, to, us_is_white); - info[i].opp_min_att_cp = (double)opp_min; - info[i].us_min_att_cp = (double)us_min; - // simple SEE for captures only - if (cap_cp > 0) { - info[i].see_cp = (double)cap_cp - (double)opp_min; - if (info[i].see_cp < -1000.0) info[i].see_cp = -1000.0; // clamp extreme - } else { - info[i].see_cp = 0.0; - } - // risk: moved into attacked square without friendly cover - if (cap_cp == 0 && opp_min > 0 && us_min == 0) { - double risk = (double)(opp_min); - if (risk > info[i].piece_cp) risk = info[i].piece_cp; - info[i].risk_cp = risk; - } else { - info[i].risk_cp = 0.0; - } - // destination geometry - int tf = file_of(to), tr = rank_of(to); - int df = tf - 3; if (df < 0) df = -df; // distance from file 'd/e' center - int dr = tr - 3; if (dr < 0) dr = -dr; // rough centrality measure - double cent = 1.0 - 0.125 * (df + dr); // max 1.0 near center, drops outward - if (cent < 0.0) cent = 0.0; - info[i].to_central = cent; - int mover_is_white = is_white(after.squares[to]); - int rank_idx = tr; // 0=a1 rank1 ... 7=h8 rank8 - info[i].rook_on_7th = 0.0; - if (tolower((unsigned char)landed) == 'r') { - if (mover_is_white && rank_idx == 6) info[i].rook_on_7th = 1.0; // 7th rank for white - if (!mover_is_white && rank_idx == 1) info[i].rook_on_7th = 1.0; // 2nd rank for black - } - } - // check to opponent's king after move - int opp_white = !board.white_to_move; // after our move, opponent is opp_white - int opp_king_sq = find_king(&after, opp_white); - int gives_check = 0; - if (opp_king_sq >= 0) { - gives_check = sq_attacked_by(&after, opp_king_sq, after.white_to_move); // side to move after = opponent; attack by our side is !after.white_to_move - // Correct attack color: our side is !after.white_to_move - gives_check = sq_attacked_by(&after, opp_king_sq, !after.white_to_move); - // attackers and mobility - int atk_cnt = count_attackers(&after, opp_king_sq, !after.white_to_move); - // mobility: count safe adjacent squares for opponent king - int tf = file_of(opp_king_sq), tr = rank_of(opp_king_sq); - int mob = 0; - for (int df = -1; df <= 1; ++df) for (int dr = -1; dr <= 1; ++dr) if (df || dr) { - int f = tf + df, r = tr + dr; - if (!on_board(f, r)) continue; - int idx = idx_from_fr(f, r); - char occ = after.squares[idx]; - if (occ != '.' && (opp_white ? is_white(occ) : is_black(occ))) continue; // own piece there - // square safe if not attacked by our side - if (!sq_attacked_by(&after, idx, !after.white_to_move)) mob++; - } - info[i].atk_opp_king = (double)atk_cnt; - info[i].opp_king_mob = (double)mob; - // prox to king - if (parse_uci_move(info[i].uci, &from, &to, &pr)) { - int kf = file_of(opp_king_sq), kr = rank_of(opp_king_sq); - int tf2 = file_of(to), tr2 = rank_of(to); - int df = tf2 - kf; if (df < 0) df = -df; - int dr = tr2 - kr; if (dr < 0) dr = -dr; - if (df <= 1 && dr <= 1) info[i].prox_king = 1.0; - // characterize the check for blockability when delivered by a slider from the moved square - char landed = after.squares[to]; - int is_slider = (tolower((unsigned char)landed) == 'r') || (tolower((unsigned char)landed) == 'b') || (tolower((unsigned char)landed) == 'q'); - info[i].checker_is_slider = is_slider ? 1 : 0; - info[i].line_check_blockable = 0; - if (gives_check && is_slider) { - int df0 = file_of(to) - file_of(opp_king_sq); - int dr0 = rank_of(to) - rank_of(opp_king_sq); - int adf = df0 < 0 ? -df0 : df0; - int adr = dr0 < 0 ? -dr0 : dr0; - int stepi = 0, stepj = 0; - if (adf == adr) { stepi = (df0 > 0) ? 1 : -1; stepj = (dr0 > 0) ? 1 : -1; } - else if (df0 == 0 && adr > 0) { stepi = 0; stepj = (dr0 > 0) ? 1 : -1; } - else if (dr0 == 0 && adf > 0) { stepi = (df0 > 0) ? 1 : -1; stepj = 0; } - int gap = 0; - if (stepi != 0 || stepj != 0) { - int fcur = file_of(opp_king_sq) + stepi; - int rcur = rank_of(opp_king_sq) + stepj; - while (on_board(fcur, rcur)) { - int idx = idx_from_fr(fcur, rcur); - if (idx == to) break; - gap++; - fcur += stepi; rcur += stepj; - } - } - if (gap >= 1) info[i].line_check_blockable = 1; - } - } - // crude mate-ish detection - if (gives_check && mob == 0) { - info[i].mate = 1; - } - } - info[i].chk = gives_check ? 1 : 0; - // baseline threat snapshot (same for all moves) - info[i].base_opp_threat_our_q = base_opp_threat_our_q; - info[i].base_opp_threat_our_r = base_opp_threat_our_r; - // threats on enemy heavy pieces after move - int our_is_white = !after.white_to_move; - for (int sq = 0; sq < 64; ++sq) { - char p = after.squares[sq]; - if (p == '.') continue; - if (opp_white ? is_white(p) : is_black(p)) { - if (tolower((unsigned char)p) == 'q') { - if (count_attackers(&after, sq, our_is_white) > 0) info[i].threat_q = 1.0; - } else if (tolower((unsigned char)p) == 'r') { - if (count_attackers(&after, sq, our_is_white) > 0) info[i].threat_r += 0.5; // multiple rooks stack - } - } - } - // opponent threats on our heavy pieces after move - int opp_is_white2 = after.white_to_move; // opponent color - for (int sq = 0; sq < 64; ++sq) { - char p2 = after.squares[sq]; - if (p2 == '.') continue; - if (our_is_white ? is_white(p2) : is_black(p2)) { - int tl = (int)tolower((unsigned char)p2); - if (tl == 'q') { - int opp_atk = count_attackers(&after, sq, opp_is_white2); - int our_def = count_attackers(&after, sq, our_is_white); - if (opp_atk > 0) info[i].opp_threat_our_q = 1.0; - if (our_def > 0) info[i].our_q_def = (double)our_def; - } else if (tl == 'r') { - int opp_atk = count_attackers(&after, sq, opp_is_white2); - int our_def = count_attackers(&after, sq, our_is_white); - if (opp_atk > 0) info[i].opp_threat_our_r += 0.5; - if (our_def > 0) info[i].our_r_def += 0.5; - } - } - } - info[i].mate = 0; // mate detection omitted in this minimal version - unsigned int local = seed ^ (unsigned int)i * 2654435761u; - info[i].score = heuristic_score(&info[i], local); - } else if (info[i].has_anno) { - unsigned int local = seed ^ (unsigned int)i * 2654435761u; - info[i].score = heuristic_score(&info[i], local); - } else { - info[i].score = (double)rand() / (double)RAND_MAX; - } - } - - // Post-pass: if we are in check, upweight moves that give check back (box checks), and - // slightly downweight immediate queen captures that don't resolve king safety. - if (have_pos && side_in_check) { - double best_box = -1e300; - int best_box_idx = -1; - for (int i = 0; i < n; ++i) { - if (info[i].chk) { - double adj = 150.0; - if (info[i].opp_king_mob <= 0.0) adj += 400.0; - info[i].score += adj; - if (info[i].score > best_box) { best_box = info[i].score; best_box_idx = i; } - } - if (info[i].cap_cp > 0.0 && info[i].piece_cp >= 850.0 && info[i].opp_min_att_cp >= 500.0) { - info[i].score -= 120.0; - } - } - } - - double best_score = -1e300; - int best_idx = -1; - for (int i = 0; i < n; ++i) { - if (info[i].score > best_score) { - best_score = info[i].score; - best_idx = i; - } - } - - if (best_idx < 0) { - free(info); - fprintf(stderr, "no moves\n"); - return 1; - } - - if (!explain) { - printf("%s\n", info[best_idx].uci); - free(info); - return 0; - } - - // JSON explanation output - printf("{\n"); - printf(" \"seed\": %u,\n", seed); - if (have_pos) { - printf(" \"fen\": \"%s\",\n", fen); - printf(" \"side_to_move\": \"%s\",\n", board.white_to_move ? "white" : "black"); - printf(" \"base_material_cp\": %d,\n", base_mat); - } - printf(" \"n\": %d,\n", n); - printf(" \"moves\": ["); - for (int i = 0; i < n; ++i) { - printf("\"%s\"%s", info[i].uci, (i + 1 < n ? ", " : "")); - } - printf("],\n"); - // Detailed per-move features for debugging - printf(" \"scores\": ["); - for (int i = 0; i < n; ++i) { - printf("%.6f%s", info[i].score, (i + 1 < n ? ", " : "")); - } - printf("],\n"); - printf(" \"features\": [\n"); - for (int i = 0; i < n; ++i) { - // For transparency, include raw and signed material components via re-derivation - int from, to; char pr; - int mat_raw = 0, mat_signed = 0; - if (have_pos && parse_uci_move(info[i].uci, &from, &to, &pr)) { - Board tmp; int cc=0, pg=0; apply_move(&board, info[i].uci, &tmp, &cc, &pg); - int mat_after = material_cp(&tmp); - mat_raw = mat_after - base_mat; - mat_signed = board.white_to_move ? mat_raw : -mat_raw; - } - printf(" { \"uci\": \"%s\", \"chk\": %d, \"mate\": %d, \"in_check\": %d, \"cap_cp\": %.1f, \"prom_cp\": %.1f, \"mat_cp_signed\": %.1f, \"mat_cp_raw\": %.1f, \"opp_min_att_cp\": %.1f, \"us_min_att_cp\": %.1f, \"piece_cp\": %.1f, \"see_cp\": %.1f, \"risk_cp\": %.1f, \"atk_opp_king\": %.1f, \"opp_king_mob\": %.1f, \"threat_q\": %.1f, \"threat_r\": %.1f, \"opp_threat_our_q\": %.1f, \"opp_threat_our_r\": %.1f, \"our_q_def\": %.1f, \"our_r_def\": %.1f, \"to_central\": %.2f, \"rook_on_7th\": %.1f, \"prox_king\": %.1f, \"score\": %.6f }%s\n", - info[i].uci, info[i].chk, info[i].mate, info[i].in_check, info[i].cap_cp, info[i].prom_cp, info[i].mat_cp, (double)mat_raw, - info[i].opp_min_att_cp, info[i].us_min_att_cp, info[i].piece_cp, info[i].see_cp, info[i].risk_cp, info[i].atk_opp_king, info[i].opp_king_mob, info[i].threat_q, info[i].threat_r, info[i].opp_threat_our_q, info[i].opp_threat_our_r, info[i].our_q_def, info[i].our_r_def, info[i].to_central, info[i].rook_on_7th, info[i].prox_king, info[i].score, - (i + 1 < n ? "," : "")); - } - printf(" ],\n"); - printf(" \"chosen_index\": %d,\n", best_idx); - printf(" \"chosen_move\": \"%s\"", info[best_idx].uci); - - if (analyze_uci) { - int cand_idx = -1; - for (int i = 0; i < n; ++i) { - if (strcmp(info[i].uci, analyze_uci) == 0) { cand_idx = i; break; } - } - double cand_score = (cand_idx >= 0 ? info[cand_idx].score : -1.0); - const char *cmp = "unknown"; - if (cand_idx >= 0) { - cmp = (cand_score > best_score ? "higher" : (cand_score < best_score ? "lower" : "equal")); - } - printf( - ",\n \"analyze\": { \"candidate\": \"%s\", \"candidate_index\": %d, \"candidate_score\": %.6f, \"compare_to_chosen\": \"%s\" }\n", - analyze_uci, cand_idx, cand_score, cmp - ); - } else { - printf("\n"); - } - printf("}\n"); - free(info); - return 0; + printf("{\"chosen_index\":%d,\"chosen_move\":\"%s\",\"analyze\":{\"candidate_move\":\"%s\",\"candidate_score\":%.1f}}\n", + chosen_idx, chosen, cand, cand_score); + return 0; } + diff --git a/C/lichess_random_engine/movegen.c b/C/lichess_random_engine/movegen.c new file mode 100644 index 0000000..ead9146 --- /dev/null +++ b/C/lichess_random_engine/movegen.c @@ -0,0 +1,458 @@ +#include "movegen.h" +#include +#include +#include +#include + +static inline int on_board(int sq) { return (sq & 0x88) == 0; } +static inline int rank_of(int sq) { return sq >> 4; } +static inline int file_of(int sq) { return sq & 7; } + +static inline int color_of(Piece p){ return (p>=BP); } +static inline int is_white(Piece p){ return p>=WP && p<=WK; } +static inline int is_black(Piece p){ return p>=BP && p<=BK; } + +static Piece make_piece(char c){ + switch(c){ + case 'P': return WP; case 'N': return WN; case 'B': return WB; case 'R': return WR; case 'Q': return WQ; case 'K': return WK; + case 'p': return BP; case 'n': return BN; case 'b': return BB; case 'r': return BR; case 'q': return BQ; case 'k': return BK; + default: return EMPTY; + } +} + +static char piece_to_char(Piece p){ + switch(p){ + case WP: return 'P'; case WN: return 'N'; case WB: return 'B'; case WR: return 'R'; case WQ: return 'Q'; case WK: return 'K'; + case BP: return 'p'; case BN: return 'n'; case BB: return 'b'; case BR: return 'r'; case BQ: return 'q'; case BK: return 'k'; + default: return '.'; + } +} + +void set_startpos(Position *pos){ + memset(pos, 0, sizeof(*pos)); + for(int i=0;iboard[i]=EMPTY; + const char *start = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; + char fen[128]; strcpy(fen, start); + strcat(fen, " w KQkq - 0 1"); + parse_fen(pos, fen); +} + +int parse_fen(Position *pos, const char *fen){ + memset(pos, 0, sizeof(*pos)); + for(int i=0;iboard[i]=EMPTY; + pos->ep_square = -1; + pos->castle = 0; + pos->halfmove_clock = 0; + pos->fullmove_number = 1; + + // pieces + int sq = 0x70; // A8 + const char *p = fen; + while (*p && *p!=' ') { + if (*p=='/') { sq = (sq & 0x70) - 0x10; p++; continue; } + if (isdigit((unsigned char)*p)) { sq += (*p - '0'); p++; continue; } + Piece pc = make_piece(*p++); + if (!on_board(sq)) return 0; + pos->board[sq++] = pc; + } + if (*p!=' ') return 0; + p++; + + // side + if (*p=='w') pos->side = WHITE; else if (*p=='b') pos->side = BLACK; else return 0; p++; + if (*p!=' ') return 0; + p++; + + // castling + if (*p=='-') { p++; } + else { + while (*p && *p!=' ') { + if (*p=='K') pos->castle |= 1<<0; + else if (*p=='Q') pos->castle |= 1<<1; + else if (*p=='k') pos->castle |= 1<<2; + else if (*p=='q') pos->castle |= 1<<3; + else return 0; + p++; + } + } + if (*p!=' ') return 0; + p++; + + // en-passant + if (*p=='-') { pos->ep_square = -1; p++; } + else { + if (p[0]>='a' && p[0]<='h' && p[1]>='1' && p[1]<='8'){ + int f = p[0]-'a'; int r = p[1]-'1'; + pos->ep_square = (r<<4) | f; + p+=2; + } else return 0; + } + if (*p==' ') p++; + + // halfmove clock + if (isdigit((unsigned char)*p)) { + pos->halfmove_clock = strtol(p, (char**)&p, 10); + } + if (*p==' ') p++; + + // fullmove number + if (isdigit((unsigned char)*p)) pos->fullmove_number = strtol(p, NULL, 10); + + return 1; +} + +static int add_move(Move *moves, int count, int max, int from, int to, int cap, int promo, int ep, int castle){ + if (count>=max) return count; + Move m; m.from=(uint8_t)from; m.to=(uint8_t)to; m.promo=(uint8_t)promo; m.is_capture=(uint8_t)cap; m.is_enpassant=(uint8_t)ep; m.is_castle=(uint8_t)castle; + moves[count++] = m; + return count; +} + +// Check detection via attack lookup +static int square_attacked_by(const Position *pos, int sq, Color by){ + // Knights + static const int kn[] = {33,31,18,14,-33,-31,-18,-14}; + for(int i=0;i<8;i++){ + int s = sq + kn[i]; + if (!on_board(s)) continue; + Piece p = pos->board[s]; + if (by==WHITE && p==WN) return 1; + if (by==BLACK && p==BN) return 1; + } + // Kings + static const int kd[] = {1,-1,16,-16,17,15,-17,-15}; + for(int i=0;i<8;i++){ + int s = sq + kd[i]; if (!on_board(s)) continue; Piece p=pos->board[s]; + if (by==WHITE && p==WK) return 1; + if (by==BLACK && p==BK) return 1; + } + // Pawns + if (by==WHITE){ + int s1 = sq - 15; int s2 = sq - 17; // white pawns attack up-left/up-right from their perspective + if (on_board(s1) && pos->board[s1]==WP) return 1; + if (on_board(s2) && pos->board[s2]==WP) return 1; + } else { + int s1 = sq + 15; int s2 = sq + 17; + if (on_board(s1) && pos->board[s1]==BP) return 1; + if (on_board(s2) && pos->board[s2]==BP) return 1; + } + // Sliders: bishops/queens diagonals + static const int bd[] = {17,15,-17,-15}; + for (int d=0; d<4; ++d){ + int s = sq + bd[d]; + while(on_board(s)){ + Piece p = pos->board[s]; + if (p!=EMPTY){ + if (by==WHITE && (p==WB || p==WQ)) return 1; + if (by==BLACK && (p==BB || p==BQ)) return 1; + break; + } + s += bd[d]; + } + } + // Rooks/queens + static const int rd[] = {1,-1,16,-16}; + for (int d=0; d<4; ++d){ + int s = sq + rd[d]; + while(on_board(s)){ + Piece p = pos->board[s]; + if (p!=EMPTY){ + if (by==WHITE && (p==WR || p==WQ)) return 1; + if (by==BLACK && (p==BR || p==BQ)) return 1; + break; + } + s += rd[d]; + } + } + return 0; +} + +int in_check(const Position *pos, Color side){ + // find king square + Piece k = (side==WHITE)?WK:BK; + int ks = -1; + for (int sq=0; sqboard[sq]==k){ ks=sq; break; } } + if (ks<0) return 0; + return square_attacked_by(pos, ks, (side==WHITE)?BLACK:WHITE); +} + +static int gen_moves_internal(const Position *pos, Move *moves, int max_moves, int captures_only){ + int count = 0; + Color us = pos->side; + int forward = (us==WHITE)?16:-16; + int start_rank = (us==WHITE)?1:6; + int promo_rank = (us==WHITE)?6:1; // rank before promotion move (from rank) + + for (int sq=0; sqboard[sq]; + if (p==EMPTY) continue; + if ((us==WHITE && !is_white(p)) || (us==BLACK && !is_black(p))) continue; + + switch(p){ + case WP: case BP: { + int dir = (p==WP)?16:-16; + int r = rank_of(sq); + // quiet pushes + if (!captures_only){ + int to = sq + dir; if (on_board(to) && pos->board[to]==EMPTY){ + if (r==promo_rank){ + count = add_move(moves, count, max_moves, sq, to, 0, (us==WHITE?WQ:BQ), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 0, (us==WHITE?WR:BR), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 0, (us==WHITE?WB:BB), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 0, (us==WHITE?WN:BN), 0, 0); + } else { + count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); + // double push from start rank + if (r==start_rank){ + int to2 = to + dir; if (on_board(to2) && pos->board[to2]==EMPTY){ + count = add_move(moves, count, max_moves, sq, to2, 0, 0, 0, 0); + } + } + } + } + } + // captures + int caps[2] = { sq + dir + 1, sq + dir - 1 }; + for (int i=0;i<2;i++){ + int to = caps[i]; if (!on_board(to)) continue; Piece tp = pos->board[to]; + if (tp!=EMPTY && color_of(tp)!=us){ + if (r==promo_rank){ + count = add_move(moves, count, max_moves, sq, to, 1, (us==WHITE?WQ:BQ), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 1, (us==WHITE?WR:BR), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 1, (us==WHITE?WB:BB), 0, 0); + count = add_move(moves, count, max_moves, sq, to, 1, (us==WHITE?WN:BN), 0, 0); + } else { + count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); + } + } + } + // en-passant + if (pos->ep_square>=0){ + for (int i=0;i<2;i++){ + int to = caps[i]; if (!on_board(to)) continue; + if (to==pos->ep_square){ + count = add_move(moves, count, max_moves, sq, to, 1, 0, 1, 0); + } + } + } + } break; + case WN: case BN: { + static const int d[8] = {33,31,18,14,-33,-31,-18,-14}; + for (int i=0;i<8;i++){ + int to = sq + d[i]; if (!on_board(to)) continue; Piece tp=pos->board[to]; + if (tp==EMPTY) { if (!captures_only) count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); } + else if (color_of(tp)!=us) count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); + } + } break; + case WB: case BB: case WR: case BR: case WQ: case BQ: { + static const int bd[4] = {17,15,-17,-15}; + static const int rd[4] = {1,-1,16,-16}; + const int *dirs = NULL; int ndirs=0; + if (p==WB || p==BB) { dirs=bd; ndirs=4; } + else if (p==WR || p==BR) { dirs=rd; ndirs=4; } + else { // queen + // iterate both sets + for (int i=0;i<4;i++){ + int to = sq + bd[i]; + while(on_board(to)){ + Piece tp=pos->board[to]; + if (tp==EMPTY) { if (!captures_only) count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); } + else { if (color_of(tp)!=us) count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); break; } + to += bd[i]; + } + } + for (int i=0;i<4;i++){ + int to = sq + rd[i]; + while(on_board(to)){ + Piece tp=pos->board[to]; + if (tp==EMPTY) { if (!captures_only) count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); } + else { if (color_of(tp)!=us) count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); break; } + to += rd[i]; + } + } + break; + } + for (int i=0;iboard[to]; + if (tp==EMPTY) { if (!captures_only) count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); } + else { if (color_of(tp)!=us) count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); break; } + to += dirs[i]; + } + } + } break; + case WK: case BK: { + static const int kd[8] = {1,-1,16,-16,17,15,-17,-15}; + for (int i=0;i<8;i++){ + int to = sq + kd[i]; if (!on_board(to)) continue; Piece tp=pos->board[to]; + if (tp==EMPTY) { if (!captures_only) count = add_move(moves, count, max_moves, sq, to, 0, 0, 0, 0); } + else if (color_of(tp)!=us) count = add_move(moves, count, max_moves, sq, to, 1, 0, 0, 0); + } + // castling (very basic, no check-through validation here; filter later) + if (!captures_only){ + // Only if not currently in check and path squares are not attacked + Color them = (us==WHITE)?BLACK:WHITE; + if (us==WHITE){ + if ((pos->castle & (1<<0)) && pos->board[0x04]==WK && pos->board[0x05]==EMPTY && pos->board[0x06]==EMPTY){ + if (!in_check(pos, WHITE) && !square_attacked_by(pos, 0x05, them) && !square_attacked_by(pos, 0x06, them)) + count = add_move(moves, count, max_moves, sq, 0x06, 0, 0, 0, 1); + } + if ((pos->castle & (1<<1)) && pos->board[0x03]==EMPTY && pos->board[0x02]==EMPTY && pos->board[0x01]==EMPTY){ + if (!in_check(pos, WHITE) && !square_attacked_by(pos, 0x03, them) && !square_attacked_by(pos, 0x02, them)) + count = add_move(moves, count, max_moves, sq, 0x02, 0, 0, 0, 1); + } + } else { + if ((pos->castle & (1<<2)) && pos->board[0x74]==BK && pos->board[0x75]==EMPTY && pos->board[0x76]==EMPTY){ + if (!in_check(pos, BLACK) && !square_attacked_by(pos, 0x75, them) && !square_attacked_by(pos, 0x76, them)) + count = add_move(moves, count, max_moves, sq, 0x76, 0, 0, 0, 1); + } + if ((pos->castle & (1<<3)) && pos->board[0x73]==EMPTY && pos->board[0x72]==EMPTY && pos->board[0x71]==EMPTY){ + if (!in_check(pos, BLACK) && !square_attacked_by(pos, 0x73, them) && !square_attacked_by(pos, 0x72, them)) + count = add_move(moves, count, max_moves, sq, 0x72, 0, 0, 0, 1); + } + } + } + } break; + default: break; + } + } + + return count; +} + +int gen_moves_pseudo(const Position *pos, Move *moves, int max_moves, int captures_only){ + return gen_moves_internal(pos, moves, max_moves, captures_only); +} + +int gen_moves(const Position *pos, Move *moves, int max_moves, int captures_only){ + int count = gen_moves_internal(pos, moves, max_moves, captures_only); + // Filter illegal moves leaving our king in check + for (int i=0;iside); + if (illegal){ + moves[i] = moves[count-1]; + count--; + } else { + i++; + } + } + return count; +} + +void make_move(Position *pos, const Move *m, Piece *captured_out){ + Piece fromP = pos->board[m->from]; + Piece toP = pos->board[m->to]; + *captured_out = toP; + + // en-passant capture + if (m->is_enpassant){ + int cap_sq = (pos->side==WHITE) ? (m->to - 16) : (m->to + 16); + *captured_out = pos->board[cap_sq]; + pos->board[cap_sq] = EMPTY; + } + + // move piece + pos->board[m->to] = fromP; + pos->board[m->from] = EMPTY; + + // promotion + if (m->promo){ pos->board[m->to] = (Piece)m->promo; } + + // castling rook move + if (m->is_castle){ + if (fromP==WK && m->to==0x06){ pos->board[0x05]=WR; pos->board[0x07]=EMPTY; } + else if (fromP==WK && m->to==0x02){ pos->board[0x03]=WR; pos->board[0x00]=EMPTY; } + else if (fromP==BK && m->to==0x76){ pos->board[0x75]=BR; pos->board[0x77]=EMPTY; } + else if (fromP==BK && m->to==0x72){ pos->board[0x73]=BR; pos->board[0x70]=EMPTY; } + } + + // update castling rights conservatively + if (fromP==WK){ pos->castle &= ~(1<<0); pos->castle &= ~(1<<1); } + if (fromP==BK){ pos->castle &= ~(1<<2); pos->castle &= ~(1<<3); } + if (m->from==0x00 || m->to==0x00) pos->castle &= ~(1<<1); + if (m->from==0x07 || m->to==0x07) pos->castle &= ~(1<<0); + if (m->from==0x70 || m->to==0x70) pos->castle &= ~(1<<3); + if (m->from==0x77 || m->to==0x77) pos->castle &= ~(1<<2); + + // en-passant square + pos->ep_square = -1; + if (fromP==WP && (m->to - m->from)==32) pos->ep_square = m->from + 16; + if (fromP==BP && (m->from - m->to)==32) pos->ep_square = m->from - 16; + + // halfmove clock + if (fromP==WP || fromP==BP || m->is_capture) pos->halfmove_clock = 0; else pos->halfmove_clock++; + + // side to move + pos->side = (pos->side==WHITE)?BLACK:WHITE; + if (pos->side==WHITE) pos->fullmove_number++; +} + +void unmake_move(Position *pos, const Move *m, Piece captured){ + pos->side = (pos->side==WHITE)?BLACK:WHITE; + if (pos->side==BLACK) pos->fullmove_number--; + + Piece moved = pos->board[m->to]; + + // undo castling rook move + if (m->is_castle){ + if (moved==WK && m->to==0x06){ pos->board[0x07]=WR; pos->board[0x05]=EMPTY; } + else if (moved==WK && m->to==0x02){ pos->board[0x00]=WR; pos->board[0x03]=EMPTY; } + else if (moved==BK && m->to==0x76){ pos->board[0x77]=BR; pos->board[0x75]=EMPTY; } + else if (moved==BK && m->to==0x72){ pos->board[0x70]=BR; pos->board[0x73]=EMPTY; } + } + + // undo promotion + if (m->promo){ moved = (pos->side==WHITE)?WP:BP; } + + pos->board[m->from] = moved; + if (m->is_enpassant){ + pos->board[m->to] = EMPTY; + int cap_sq = (pos->side==WHITE) ? (m->to - 16) : (m->to + 16); + pos->board[cap_sq] = captured; + } else { + pos->board[m->to] = captured; + } + + // Note: We do not restore previous castle/ep/halfmove here (for perft driver we will handle state by copying Position before make_move) + // For correctness in deeper engine, we’d need a move stack with state; perft here uses position copies for make/unmake. + // To keep unmake consistent for our usage (make->unmake on a copy), we keep simple. +} + +int square_from_algebraic(const char *uci4, int is_from){ + // uci like e2e4 or e7e8q + if (!uci4 || strlen(uci4) < 4) return -1; + int f = uci4[is_from?0:2] - 'a'; + int r = uci4[is_from?1:3] - '1'; + if (f<0||f>7||r<0||r>7) return -1; + return (r<<4)|f; +} + +int move_from_uci(const Position *pos, const char *uci, Move *out){ + int from = square_from_algebraic(uci, 1); + int to = square_from_algebraic(uci, 0); + if (from<0||to<0) return 0; + char promo = 0; if (strlen(uci)>=5) promo = uci[4]; + Move moves[256]; + int n = gen_moves(pos, moves, 256, 0); + for (int i=0;iside==WHITE)?WQ:BQ; + else if (promo=='r' || promo=='R') pp = (pos->side==WHITE)?WR:BR; + else if (promo=='b' || promo=='B') pp = (pos->side==WHITE)?WB:BB; + else if (promo=='n' || promo=='N') pp = (pos->side==WHITE)?WN:BN; + if (pp && pp==moves[i].promo){ *out = moves[i]; return 1; } + } else { + if (!promo){ *out = moves[i]; return 1; } + } + } + } + return 0; +} diff --git a/C/lichess_random_engine/movegen.h b/C/lichess_random_engine/movegen.h new file mode 100644 index 0000000..15efa0f --- /dev/null +++ b/C/lichess_random_engine/movegen.h @@ -0,0 +1,50 @@ +#ifndef MOVEGEN_H +#define MOVEGEN_H + +#include + +// 0x88 board representation +#define BOARD_SIZE 128 + +typedef enum { WHITE = 0, BLACK = 1 } Color; + +typedef enum { + EMPTY = 0, + WP = 1, WN, WB, WR, WQ, WK, + BP = 7, BN, BB, BR, BQ, BK +} Piece; + +typedef struct { + // from and to squares in 0x88 (0..127), promotion piece in Piece enum or 0 + uint8_t from, to; + uint8_t promo; // 0 if none + uint8_t is_capture; // 1 if capture + uint8_t is_enpassant; // 1 if en-passant capture + uint8_t is_castle; // 1 if castle +} Move; + +typedef struct { + Piece board[BOARD_SIZE]; + Color side; + // Castling rights: bit 0 white king-side, 1 white queen-side, 2 black king-side, 3 black queen-side + uint8_t castle; + int8_t ep_square; // -1 if none, else 0x88 square index + int halfmove_clock; + int fullmove_number; +} Position; + +// Parsing and utilities +int parse_fen(Position *pos, const char *fen); +void set_startpos(Position *pos); +int square_from_algebraic(const char *uci4, int is_from); +int move_from_uci(const Position *pos, const char *uci, Move *out); +void make_move(Position *pos, const Move *m, Piece *captured_out); +void unmake_move(Position *pos, const Move *m, Piece captured); +int in_check(const Position *pos, Color side); + +// Move generation +// Generates all pseudo-legal moves into moves[], returns count. If captures_only!=0, only captures (incl. ep) are generated +int gen_moves(const Position *pos, Move *moves, int max_moves, int captures_only); +int gen_moves_pseudo(const Position *pos, Move *moves, int max_moves, int captures_only); + +#endif // MOVEGEN_H \ No newline at end of file diff --git a/C/lichess_random_engine/perft b/C/lichess_random_engine/perft new file mode 100755 index 0000000..8546b95 Binary files /dev/null and b/C/lichess_random_engine/perft differ diff --git a/C/lichess_random_engine/perft.c b/C/lichess_random_engine/perft.c new file mode 100644 index 0000000..9869f36 --- /dev/null +++ b/C/lichess_random_engine/perft.c @@ -0,0 +1,72 @@ +#include "movegen.h" +#include +#include +#include + +static unsigned long long perft(Position pos, int depth){ + if (depth==0) return 1ULL; + Move moves[256]; + unsigned long long nodes = 0ULL; + int n = gen_moves(&pos, moves, 256, 0); + for (int i=0;ifrom & 7), fr = (m->from >> 4); + int tf = (m->to & 7), tr = (m->to >> 4); + buf[0] = 'a' + ff; buf[1] = '1' + fr; buf[2] = 'a' + tf; buf[3] = '1' + tr; int i=4; + if (m->promo){ char pc='q'; switch(m->promo){ case WQ: case BQ: pc='q'; break; case WR: case BR: pc='r'; break; case WB: case BB: pc='b'; break; case WN: case BN: pc='n'; break; default: pc='q'; } + buf[i++]=pc; } + buf[i]=0; +} + +static void run_case(const char *fen, int depth, unsigned long long expected){ + Position p; if (!parse_fen(&p, fen)){ fprintf(stderr, "Bad FEN: %s\n", fen); return; } + unsigned long long n = perft(p, depth); + printf("perft(%d) = %llu %s\n", depth, n, (expected? (n==expected?"OK":"MISMATCH"):"")); +} + +int main(int argc, char**argv){ + if (argc>=3){ + const char *fen = argv[1]; + int depth = atoi(argv[2]); + Position p; if (!parse_fen(&p, fen)){ fprintf(stderr, "Bad FEN input\n"); return 2; } + if (argc>=4 && strcmp(argv[3], "--divide")==0){ + Move moves[256]; int n = gen_moves(&p, moves, 256, 0); + unsigned long long total=0ULL; + for (int i=0;i=4 && strcmp(argv[3], "--divide-pseudo")==0){ + Move moves[256]; int n = gen_moves_pseudo(&p, moves, 256, 0); + for (int i=0;i +#include + +static int piece_value(Piece p){ + switch(p){ + case WP: case BP: return 100; + case WN: case BN: return 320; + case WB: case BB: return 330; + case WR: case BR: return 500; + case WQ: case BQ: return 900; + case WK: case BK: return 0; // king is invaluable; PST handled later if needed + default: return 0; + } +} + +int evaluate(const Position *pos){ + int score = 0; + for (int sq=0; sqboard[sq]; + if (p==EMPTY) continue; + int v = piece_value(p); + if (p>=WP && p<=WK) score += v; else if (p>=BP && p<=BK) score -= v; + } + // Score from side-to-move perspective + return (pos->side==WHITE)? score : -score; +} + +int alphabeta(Position pos, int depth, int alpha, int beta, int *pv_from, int *pv_to){ + if (depth<=0){ + return evaluate(&pos); + } + Move moves[256]; + int n = gen_moves(&pos, moves, 256, 0); + if (n==0){ + // Checkmate or stalemate + if (in_check(&pos, pos.side)) return -30000 + (10 - depth); // checkmated + return 0; // stalemate + } + + int best_score = INT_MIN/2; + int best_from = -1, best_to = -1; + for (int i=0;i best_score){ + best_score = score; + best_from = moves[i].from; + best_to = moves[i].to; + } + if (best_score > alpha) alpha = best_score; + if (alpha >= beta) break; // beta cutoff + } + if (pv_from) *pv_from = best_from; + if (pv_to) *pv_to = best_to; + return best_score; +} diff --git a/C/lichess_random_engine/search.h b/C/lichess_random_engine/search.h new file mode 100644 index 0000000..9ea96d1 --- /dev/null +++ b/C/lichess_random_engine/search.h @@ -0,0 +1,17 @@ +#ifndef SEARCH_H +#define SEARCH_H + +#include "movegen.h" + +typedef struct { + int depth; + int nodes; +} SearchLimits; + +// Evaluate position in centipawns from the side-to-move perspective. +int evaluate(const Position *pos); + +// Negamax alpha-beta returning score in centipawns from side-to-move perspective. +int alphabeta(Position pos, int depth, int alpha, int beta, int *pv_from, int *pv_to); + +#endif // SEARCH_H \ No newline at end of file diff --git a/PYTHON/.gitignore b/PYTHON/.gitignore index 291fd4f..592fd7a 100644 --- a/PYTHON/.gitignore +++ b/PYTHON/.gitignore @@ -213,4 +213,6 @@ marimo/_lsp/ __marimo__/ # Streamlit -.streamlit/secrets.toml \ No newline at end of file +.streamlit/secrets.toml + +*lichess_db_puzzle.csv* \ No newline at end of file diff --git a/PYTHON/lichess_bot/.bot_version b/PYTHON/lichess_bot/.bot_version index 72f523f..ac4213d 100644 --- a/PYTHON/lichess_bot/.bot_version +++ b/PYTHON/lichess_bot/.bot_version @@ -1 +1 @@ -39 \ No newline at end of file +43 \ No newline at end of file