From 02b1ca4cfeefaaf55a36a5d97308e9f82c93184e Mon Sep 17 00:00:00 2001 From: Ole Morud Date: Fri, 19 Dec 2025 04:01:32 +0100 Subject: [PATCH] Make engine dep-free/standalone and add Wasm target --- Makefile | 15 +- base.h | 532 ++++++++++++++++---------------------------------- board_print.h | 244 +++++++++++++++++++++++ chess.html | 35 ++++ chess.js | 232 ++++++++++++++++++++++ evaluations.h | 202 +++++++++++++++++++ libc-lite.h | 80 ++++++++ sys.h | 194 ++++++++++++++++++ tests.c | 51 +++-- wasm-compat.c | 71 +++++++ 10 files changed, 1264 insertions(+), 392 deletions(-) create mode 100644 board_print.h create mode 100644 chess.html create mode 100644 chess.js create mode 100644 evaluations.h create mode 100644 libc-lite.h create mode 100644 sys.h create mode 100644 wasm-compat.c diff --git a/Makefile b/Makefile index 4ba256b..5d5eef4 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,17 @@ BUILD ?= debug CC := clang -CFLAGS.gcc.release := -O3 -CFLAGS.gcc.debug := -ggdb -O0 -fsanitize=address CFLAGS.gcc := -std=c23 -Wall -Wextra -Wconversion -Wno-unused-function +CFLAGS.gcc.release := -Ofast +CFLAGS.gcc.debug := -ggdb -O0 -fsanitize=address -CFLAGS.clang.release := -O3 -CFLAGS.clang.debug := -ggdb -O0 -fsanitize=address CFLAGS.clang := -std=c23 -Wall -Wextra -Wconversion -Wno-unused-function -Wimplicit-int-conversion +CFLAGS.clang.release := -Ofast +CFLAGS.clang.debug := -ggdb -O0 -fsanitize=address +CFLAGS.clang.wasm := \ + --target=wasm32-unknown-unknown -O3 -nostdlib \ + -Wl,--export-all \ + -Wl,--no-entry CFLAGS := $(CFLAGS.$(CC)) $(CFLAGS.$(CC).$(BUILD)) @@ -17,6 +21,9 @@ all: tests codegen: codegen.c $(CC) -o $@ $(CFLAGS) $^ +wasm: wasm-compat.c + $(CC) -DWASM -o chess.wasm wasm-compat.c $(CFLAGS.$(CC)) $(CFLAGS.$(CC).wasm) + mbb_rook.h: codegen ./codegen diff --git a/base.h b/base.h index 7f1612a..0fb5a53 100644 --- a/base.h +++ b/base.h @@ -1,32 +1,16 @@ #pragma once -#include -#include -#include +#include "libc-lite.h" +#include "sys.h" + #include -#include -#include -#include -#include -#define index string_index -#include -#undef index - -static struct { - /* all globals are zero by default in C */ - uint64_t tt_collisions; - uint64_t tt_hits; - uint64_t tt_probes; -} global_stats; - -static void print_stats(FILE* out) -{ - fprintf(out, "Stats:\n"); - fprintf(out, "tt collisions: %lu\n", global_stats.tt_collisions); - fprintf(out, "tt hits: %lu\n", global_stats.tt_hits); - fprintf(out, "tt probes: %lu\n", global_stats.tt_probes); -} +#ifndef NDEBUG +#define assert(expr) \ + ((expr) ? 0 : (__builtin_trap(), 0)) +#else +#define assert(...) (void)0 +#endif /* BIT MANIPULATION * ================ */ @@ -43,23 +27,6 @@ static inline uint64_t ctz(uint64_t n) return (uint64_t)__builtin_ctzll(n); } -/**/ -static uint64_t rand64() { - union { - uint64_t v; - uint16_t vs[4]; - } x = { - .vs = { - (uint16_t)rand(), - (uint16_t)rand(), - (uint16_t)rand(), - (uint16_t)rand(), - } - }; - - return x.v; -} - /* BITBOARD AND INDEX DATATYPES AND HELPERS * ========================================= */ @@ -86,12 +53,9 @@ h2##g2##f2##e2##d2##c2##b2##a2##\ h1##g1##f1##e1##d1##c1##b1##a1##\ ULL -#define BITBOARD_FMT PRIu64 -#define BITBOARD_FMT_X PRIx64 typedef uint8_t index; #define INDEX(n) n##ULL -#define INDEX_FMT PRIu8 #define RANK_SHIFT_UP(b, n) ((b) << ((index)(n)*8U)) #define RANK_SHIFT_DOWN(b, n) ((b) >> ((index)(n)*8U)) @@ -124,6 +88,17 @@ enum file_index : index { FILE_INDEX_COUNT, }; +char const file_index_char[FILE_INDEX_COUNT] = { + [FILE_INDEX_A] = 'A', + [FILE_INDEX_B] = 'B', + [FILE_INDEX_C] = 'C', + [FILE_INDEX_D] = 'D', + [FILE_INDEX_E] = 'E', + [FILE_INDEX_F] = 'F', + [FILE_INDEX_G] = 'G', + [FILE_INDEX_H] = 'H', +}; + enum rank_index : index { RANK_INDEX_BEGIN, RANK_INDEX_1 = RANK_INDEX_BEGIN, @@ -137,6 +112,17 @@ enum rank_index : index { RANK_INDEX_COUNT, }; +char const rank_index_char[RANK_INDEX_COUNT] = { + [RANK_INDEX_1] = '1', + [RANK_INDEX_2] = '2', + [RANK_INDEX_3] = '3', + [RANK_INDEX_4] = '4', + [RANK_INDEX_5] = '5', + [RANK_INDEX_6] = '6', + [RANK_INDEX_7] = '7', + [RANK_INDEX_8] = '8', +}; + #define STR(x) #x #define SQ_MASK_PREFIX SQ_MASK_ @@ -330,7 +316,7 @@ enum : bitboard { * Player * =========== * */ -enum player : size_t { +enum player : uint8_t { PLAYER_BEGIN, PLAYER_WHITE = PLAYER_BEGIN, PLAYER_BLACK, @@ -885,7 +871,16 @@ struct search_option { #define TT_ADDRESS_BITS 24 #define TT_ENTRIES (1ULL<appeal = 16*n - (uint8_t)piece_value[atk]; } -#define BOARD_INITIAL (struct board) { \ +#define BOARD_INIT (struct board) { \ .pos = { \ .fullmoves = 1, \ .pieces = { \ @@ -1006,81 +1001,6 @@ static void move_compute_appeal(struct move* m, .hist = {0}, \ } -static void board_print_fen(struct pos const* pos, FILE* out) -{ - int buf[8][8] = {0}; - - for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { - for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { - bitboard x = pos->pieces[player][piece]; - - for (index i = 7; i < 8; i--) { - for (index j = 0; j < 8; ++j) { - if (x & (1ULL<<(i*8+j))) { - buf[i][j] = piece_char[player][piece]; - } - } - } - } - } - - for (enum rank_index i = RANK_INDEX_8; i < RANK_INDEX_COUNT; --i) { - int consequtive_empty = 0; - for (enum file_index j = FILE_INDEX_A; j < FILE_INDEX_COUNT; ++j) { - if (buf[i][j]) { - if (consequtive_empty) { - fprintf(out, "%d", consequtive_empty); - consequtive_empty = 0; - } - fprintf(out, "%lc", buf[i][j]); - } else { - consequtive_empty += 1; - } - } - if (consequtive_empty) { - fprintf(out, "%d", consequtive_empty); - } - if (i > 0) - fprintf(out, "/"); - } - - fprintf(out, " %c ", pos->player == PLAYER_WHITE ? 'w' : 'b'); - - bool any_castle = false; - if (!pos->castling_illegal[PLAYER_WHITE][CASTLE_KINGSIDE]) { - fprintf(out, "K"); - any_castle = true; - } - if (!pos->castling_illegal[PLAYER_WHITE][CASTLE_QUEENSIDE]) { - fprintf(out, "Q"); - any_castle = true; - } - if (!pos->castling_illegal[PLAYER_BLACK][CASTLE_KINGSIDE]) { - fprintf(out, "k"); - any_castle = true; - } - if (!pos->castling_illegal[PLAYER_BLACK][CASTLE_QUEENSIDE]) { - fprintf(out, "q"); - any_castle = true; - } - if (!any_castle) { - fprintf(out, "-"); - } - - if (pos->ep_targets) { - /* should be ep target square in algebraic notation */ - fprintf(stderr, "not implemented: fen with en passent squares\n"); - fprintf(out, ""); - } else { - fprintf(out, " -"); - } - - fprintf(out, " %d", pos->halfmoves); - fprintf(out, " %d", pos->fullmoves); - - fprintf(out, "\n"); -} - static bool board_load_fen_unsafe(struct board* b, char const* fen_str) { /* TODO: this function is not tested for malicious/corrupted inputs */ @@ -1093,17 +1013,17 @@ static bool board_load_fen_unsafe(struct board* b, char const* fen_str) .i = 0, .len = strlen(fen_str), }; -#define BUF_GETCHAR(x) (fprintf(stderr, "[%c]", x.buf[x.i]), assert(x.i < x.len), x.buf[x.i++]) +#define BUF_GETCHAR(x) (assert(x.i < x.len), x.buf[x.i++]) - memset(&b->pos, 0, sizeof b->pos); + my_memset(&b->pos, 0, sizeof b->pos); b->pos.hash = ~0ULL; for (enum rank_index ri = RANK_INDEX_8; ri < RANK_INDEX_COUNT; --ri) { for (enum file_index fi = FILE_INDEX_BEGIN; fi < FILE_INDEX_COUNT; ++fi) { char const ch = BUF_GETCHAR(fen); - if (isdigit(ch)) { + if (my_isdigit(ch)) { if (ch == '0') { - abort(); + __builtin_trap(); } fi += ch - '0' - 1; } else { @@ -1124,14 +1044,14 @@ static bool board_load_fen_unsafe(struct board* b, char const* fen_str) } else if (ch == 'b') { b->pos.player = PLAYER_BLACK; } else { - abort(); + __builtin_trap(); } } { /* castling */ char ch = BUF_GETCHAR(fen); if (ch != ' ') { - abort(); + __builtin_trap(); } b->pos.castling_illegal[PLAYER_WHITE][CASTLE_KINGSIDE] = true; b->pos.castling_illegal[PLAYER_WHITE][CASTLE_QUEENSIDE] = true; @@ -1147,8 +1067,7 @@ static bool board_load_fen_unsafe(struct board* b, char const* fen_str) case ' ': break; case '-': break; default: { - fprintf(stderr, "unexpected char '%c'\n", ch); - abort(); + __builtin_trap(); } break; } } while (ch != ' '); @@ -1157,8 +1076,7 @@ static bool board_load_fen_unsafe(struct board* b, char const* fen_str) { /* en passent */ char const ch = BUF_GETCHAR(fen); if (ch != '-') { - fprintf(stderr, "ep targets in fen not implemented yet\n"); - abort(); + __builtin_trap(); } } @@ -1522,14 +1440,14 @@ static void init_zobrist() for (enum square_index sq = SQ_INDEX_BEGIN; sq < SQ_INDEX_COUNT; ++sq) { for (enum player pl = PLAYER_BEGIN; pl < PLAYER_COUNT; ++pl) { for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { - zobrist.piece_keys[sq][pl][piece] = rand64(); + zobrist.piece_keys[sq][pl][piece] = my_rand64(); } } - zobrist.ep_targets[sq] = rand64(); + zobrist.ep_targets[sq] = my_rand64(); } for (enum player pl = PLAYER_BEGIN; pl < PLAYER_COUNT; ++pl) { for (enum castle_direction d = CASTLE_BEGIN; d < CASTLE_COUNT; ++d) { - zobrist.castling_keys[pl][d] = rand64(); + zobrist.castling_keys[pl][d] = my_rand64(); } } zobrist.init = true; @@ -1573,10 +1491,20 @@ static inline uint64_t tt_hash_switch_player(uint64_t hash) return ~hash; } -static inline struct search_option tt_get(struct tt const* tt, uint64_t hash) +static inline struct search_option tt_get(struct tt* tt, uint64_t hash) { uint64_t const addr = hash % TT_ENTRIES; - return tt->entries[addr]; + struct search_option tte = tt->entries[addr]; + +#ifndef NDEBUG + tt->probes += 1; + if (tte.init && tte.hash == hash) { + tt->hits += 1; + } else if (tte.init && tte.hash != hash) { + tt->collisions += 1; + } +#endif + return tte; } static inline void tt_insert(struct tt* tt, uint64_t hash, struct search_option so) @@ -1584,17 +1512,19 @@ static inline void tt_insert(struct tt* tt, uint64_t hash, struct search_option uint64_t const addr = hash % TT_ENTRIES; so.init = true; tt->entries[addr] = so; +#ifndef NDEBUG + tt->insertions += 1; +#endif } enum move_result { MR_NORMAL, MR_CHECK, + MR_REPEATS, /* this board state has been observed before */ MR_STALEMATE, MR_CHECKMATE, }; -static void board_print(const struct pos* pos, struct move* move, FILE* out); - /* does not check validity */ static enum move_result board_move(struct pos* restrict pos, struct history* restrict hist, @@ -1604,20 +1534,6 @@ static enum move_result board_move(struct pos* restrict pos, enum player const us = pos->player; enum player const them = opposite_player(us); -#ifndef NDEBUG - if (SQ_MASK_FROM_INDEX(move.to) & pos->pieces[them][PIECE_KING]) { - fprintf(stderr, "fatal error: king capture\n"); - board_print_fen(pos, stderr); - board_print(pos, &move, stderr); - for (enum piece p = PIECE_BEGIN; p < PIECE_COUNT; ++p) { - if (pos->pieces[us][p] & SQ_MASK_FROM_INDEX(move.from)) { - fprintf(stderr, "%s found on %s\n", piece_str[p], square_index_display[move.from]); - } - } - __builtin_trap(); - } -#endif - enum piece const from_piece = mailbox[move.from]; enum piece const to_piece = mailbox[move.to]; @@ -1749,26 +1665,35 @@ static enum move_result board_move(struct pos* restrict pos, pos->fullmoves += (pos->player == PLAYER_BLACK); pos->halfmoves += 1; - assert(hist->length < 4096); + assert(hist->length < 64); + int repetitions = 0; for (size_t i = 0; i < hist->length; ++i) { _Static_assert(sizeof *pos == sizeof hist->items[i]); - if (!memcmp(&hist->items[i].pieces, &pos->pieces, sizeof pos->pieces) - && !memcmp(&hist->items[i].castling_illegal, &pos->castling_illegal, sizeof pos->castling_illegal) + if (!my_memcmp(&hist->items[i].pieces, &pos->pieces, sizeof pos->pieces) + && !my_memcmp(&hist->items[i].castling_illegal, &pos->castling_illegal, sizeof pos->castling_illegal) && hist->items[i].player == pos->player && hist->items[i].ep_targets == pos->ep_targets) { - return MR_STALEMATE; + repetitions += 1; } } + hist->items[hist->length++] = *pos; - if (pos->halfmoves > 50) { + if (repetitions >= 3 || pos->halfmoves > 50) { + return MR_STALEMATE; + } + else if (repetitions > 0) { + return MR_REPEATS; + } + else if (pos->halfmoves > 50) { return MR_STALEMATE; } - - if (attacks_to(pos, pos->pieces[them][PIECE_KING], 0ULL, 0ULL) & ~pos->occupied[them]) { + else if (attacks_to(pos, pos->pieces[them][PIECE_KING], 0ULL, 0ULL) + & ~pos->occupied[them]) { return MR_CHECK; - } else { + } + else { return MR_NORMAL; } } @@ -1789,41 +1714,60 @@ static enum move_result board_move_2(struct board* b, struct move move) * */ static double board_score_heuristic(struct pos const* pos) { + /* this function always evaluates from white's perspective before + eventually flipping the sign */ double score = 0.0; - #define BOARD_CENTER ((FILE_MASK_C | FILE_MASK_D | FILE_MASK_E | FILE_MASK_F) \ - & ~(RANK_MASK_1 | RANK_MASK_2 | RANK_MASK_7 | RANK_MASK_8)) - static bitboard const positional_modifier_area[PIECE_COUNT] = { - [PIECE_PAWN] = BOARD_CENTER, - [PIECE_KNIGHT] = BOARD_CENTER, - [PIECE_QUEEN] = RANK_MASK_3 | RANK_MASK_4 | RANK_MASK_5 | RANK_MASK_6, - }; - #undef BOARD_CENTER - static double const positional_modifier_factor[PIECE_COUNT] = { - [PIECE_PAWN] = 0.02, - [PIECE_KNIGHT] = 0.02, - [PIECE_QUEEN] = -0.03, - }; + /* evaluations.h defines: + - POSITIONAL_MODIFIER_COUNT + - static struct {bitboard const area; double const val} const + positional_modifier[PLAYER_COUNT][POSITIONAL_MODIFIER_COUNT][PIECE_COUNT]; + * */ +#include "evaluations.h" - bitboard const white_threats = all_threats_from_player(pos, PLAYER_WHITE); - bitboard const black_threats = all_threats_from_player(pos, PLAYER_BLACK); - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" for (enum piece p = PIECE_BEGIN; p < PIECE_COUNT; ++p) { - score += 0.002*piece_value[p] * - ((double)bitboard_popcount(white_threats & pos->pieces[PLAYER_BLACK][p]) - - (double)bitboard_popcount(black_threats & pos->pieces[PLAYER_WHITE][p])); - + /* raw material value */ score += piece_value[p] * ((double)bitboard_popcount(pos->pieces[PLAYER_WHITE][p]) - (double)bitboard_popcount(pos->pieces[PLAYER_BLACK][p])); - score += positional_modifier_factor[p] * - ((double)bitboard_popcount(pos->pieces[PLAYER_WHITE][p] & positional_modifier_area[p]) - - (double)bitboard_popcount(pos->pieces[PLAYER_BLACK][p] & positional_modifier_area[p])); + /* pawns defending pieces are desired */ + score += 0.05 * ( + (double)bitboard_popcount( + pawn_attacks_white(pos->pieces[PLAYER_WHITE][PIECE_PAWN]) + & pos->pieces[PLAYER_WHITE][p] + ) + - (double)bitboard_popcount( + pawn_attacks_black(pos->pieces[PLAYER_WHITE][PIECE_PAWN]) + & pos->pieces[PLAYER_WHITE][p] + ) + ); + + /* positional bonus, see evaluations.h */ + for (size_t i = 0; i < POSITIONAL_MODIFIER_COUNT; ++i) { + score += positional_modifier[PLAYER_WHITE][i][p].val * + ( + (double)bitboard_popcount( + pos->pieces[PLAYER_WHITE][p] + & positional_modifier[PLAYER_WHITE][i][p].area + ) + - (double)bitboard_popcount( + pos->pieces[PLAYER_BLACK][p] + & positional_modifier[PLAYER_BLACK][i][p].area + ) + ); + } + } + + /* stacked pawns are bad */ + const double k = 0.30; + for (enum file_index fi = FILE_INDEX_BEGIN; fi < FILE_INDEX_COUNT; ++fi) { + uint64_t wstk = bitboard_popcount(pos->pieces[PLAYER_WHITE][PIECE_PAWN] & FILE_MASK(fi)); + uint64_t bstk = bitboard_popcount(pos->pieces[PLAYER_BLACK][PIECE_PAWN] & FILE_MASK(fi)); + + score -= k * (double)(wstk - (wstk == 1)); + score += k * (double)(bstk - (bstk == 1)); } -#pragma clang diagnostic pop double sign = (pos->player == PLAYER_WHITE) ? 1.0 : -1.0; @@ -1870,17 +1814,11 @@ double quiesce(struct pos const* pos, size_t move_count = 0; - struct move* moves = malloc(sizeof(struct move[MOVE_MAX])); - if (!moves) { - abort(); - } - - /*struct move moves[MOVE_MAX];*/ + struct move moves[MOVE_MAX]; all_moves(pos, us, &move_count, moves); if (move_count == 0) { /* TODO: detect stalemate */ - free(moves); return -(999.0 + (double)depth); } for (size_t i = 0; i < move_count; ++i) { @@ -1893,31 +1831,18 @@ double quiesce(struct pos const* pos, if ((m.attr & MATTR_CAPTURE) == 0ULL) { continue; } + struct pos poscpy = *pos; + + enum piece mailbox_cpy[SQ_INDEX_COUNT]; + my_memcpy(mailbox_cpy, mailbox, sizeof (enum piece[SQ_INDEX_COUNT])); - /* TODO: make lean apply/undo mechanism instead of copying, - * use of malloc is particularly horrendous */ - struct pos* poscpy = malloc(sizeof *poscpy); - if (!poscpy) { - perror("malloc"); - abort(); - } - *poscpy = *pos; - enum piece* mailbox_cpy = malloc(sizeof (enum piece[SQ_INDEX_COUNT])); - if (!mailbox_cpy) { - perror("malloc"); - abort(); - } - memcpy(mailbox_cpy, mailbox, sizeof (enum piece[SQ_INDEX_COUNT])); /* history is irrelevant when all moves are captures */ static struct history hist; hist.length = 0; - (void)board_move(poscpy, &hist, mailbox_cpy, m); + (void)board_move(&poscpy, &hist, mailbox_cpy, m); - score = -quiesce(poscpy, mailbox_cpy, them, -beta, -alpha, depth - 1); - - free(poscpy); - free(mailbox_cpy); + score = -quiesce(&poscpy, mailbox_cpy, them, -beta, -alpha, depth - 1); if (score >= beta) { highscore = score; @@ -1931,29 +1856,25 @@ double quiesce(struct pos const* pos, } } - free(moves); - return highscore; } +static struct search_option alphabeta_search(struct pos const* pos, struct history* hist, struct tt* tt, enum piece mailbox[restrict static SQ_INDEX_COUNT], enum player us, int8_t depth, + uint64_t mattr_filter, double alpha, double beta) { - - const double alpha_orig = alpha; - - // Terminal / leaf if (depth <= 0) { return (struct search_option) { - /*.score = quiesce(pos, mailbox, us, alpha, beta, depth),*/ - .score = board_score_heuristic(pos), + .score = quiesce(pos, mailbox, us, alpha, beta, 0), + /*.score = board_score_heuristic(pos),*/ .move = (struct move){0}, .depth = 0, .hash = pos->hash, @@ -1962,17 +1883,9 @@ struct search_option alphabeta_search(struct pos const* pos, }; } - /* TT probe at current node */ - global_stats.tt_probes += 1; - struct search_option tte = tt_get(tt, pos->hash); + double const alpha_orig = alpha; -#ifndef NDEBUG - if (tte.init && tte.hash == pos->hash) { - global_stats.tt_hits += 1; - } else if (tte.init && tte.hash != pos->hash) { - global_stats.tt_collisions += 1; - } -#endif + struct search_option tte = tt_get(tt, pos->hash); if (tte.init && tte.hash == pos->hash && tte.depth >= depth) { if (tte.flag == TT_EXACT) { @@ -1992,7 +1905,6 @@ struct search_option alphabeta_search(struct pos const* pos, if (move_count == 0) { /* TODO: reusing mate distances correctly needs ply normalization */ - /* TODO: detect stalemate */ double score = 0; if (attacks_to(pos, pos->pieces[us][PIECE_KING], 0ULL, 0ULL) != 0ULL) { score = -(999.0 + (double)depth); @@ -2027,16 +1939,20 @@ struct search_option alphabeta_search(struct pos const* pos, while (move_count) { struct move m = moves_linear_search(moves, &move_count); + if (mattr_filter && !(m.attr & mattr_filter)) { + continue; + } + /* TODO: make lean apply/undo mechanism instead of copying */ struct pos poscpy = *pos; enum piece mailbox_cpy[SQ_INDEX_COUNT]; - memcpy(mailbox_cpy, mailbox, sizeof mailbox_cpy); + my_memcpy(mailbox_cpy, mailbox, sizeof mailbox_cpy); size_t old_hist_length = hist->length; enum move_result const r = board_move(&poscpy, hist, mailbox_cpy, m); double score; - if (r == MR_STALEMATE) { + if (r == MR_STALEMATE || r == MR_REPEATS) { score = 0.0; } else { score = -alphabeta_search(&poscpy, @@ -2045,6 +1961,7 @@ struct search_option alphabeta_search(struct pos const* pos, mailbox_cpy, opposite_player(us), depth - 1, + mattr_filter, -beta, -alpha).score; } @@ -2089,15 +2006,19 @@ struct search_option alphabeta_search(struct pos const* pos, return out; } -struct move search(struct board* b, enum player us, int8_t max_depth) +static struct search_result {struct move move; double score;} +search(struct board* b, enum player us, int8_t max_depth) { +#if 1 if (b->tt.entries == NULL) { - b->tt.entries = calloc(TT_ENTRIES, sizeof b->tt.entries[0]); + b->tt.entries = sys_mmap_anon_shared(TT_ENTRIES * sizeof b->tt.entries[0], + SYS_PROT_READ | SYS_PROT_WRITE, + SYS_MADV_RANDOM); if (b->tt.entries == NULL) { - perror("calloc"); - exit(EXIT_FAILURE); + __builtin_trap(); } } +#endif struct move best_move = {0}; @@ -2106,13 +2027,13 @@ struct move search(struct board* b, enum player us, int8_t max_depth) #define SCORE_INF 1e80 for (int8_t d = 1; d <= max_depth; ++d) { - double window = 0.5; + double window = 2.0; double alpha = score - window; double beta = score + window; while (true) { struct search_option so = - alphabeta_search(&b->pos, &b->hist, &b->tt, b->mailbox, us, d, alpha, beta); + alphabeta_search(&b->pos, &b->hist, &b->tt, b->mailbox, us, d, 0, alpha, beta); if (so.score > alpha && so.score < beta) { score = so.score; @@ -2132,136 +2053,11 @@ struct move search(struct board* b, enum player us, int8_t max_depth) } } - fprintf(stderr, "depth: %d\n", d); } #undef SCORE_INF - return best_move; + return (struct search_result){.move = best_move, .score = score}; } -static void board_print(const struct pos* pos, struct move* move, FILE* out) -{ - int buf[8][8] = {0}; - - for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { - for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { - bitboard x = pos->pieces[player][piece]; - - for (index i = 7; i < 8; i--) { - for (index j = 0; j < 8; ++j) { - if (x & (1ULL<<(i*8+j))) { - buf[i][j] = piece_unicode[player][piece]; - } - } - } - } - } - - /* see: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */ - setlocale(LC_ALL, ""); - for (index i = 7; i < 8; i--) { - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "%"INDEX_FMT" ", i+1); - for (index j = 0; j < 8; ++j) { - index const n = INDEX_FROM_RF(i,j); - if (move && n == move->from) { - fprintf(out, "\033[%d;%dm", 30, 104); /* 44: blue*/ - } else if (move && n == move->to) { - fprintf(out, "\033[%d;%dm", 30, 44); /* 104: bright blue*/ - } else { - /* 45: magenta, 47: white */ - fprintf(out, "\033[%d;%dm", 30, (i+j) % 2 ? 45 : 47); - } - if (buf[i][j]) { - fprintf(out, "%lc ", buf[i][j]); - } else { - fprintf(out, " "); /* idk why this hack is needed but "%lc " - is not working when buf[i][j] = ' ' */ - } - } - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "\n"); - } - fprintf(out, " A B C D E F G H \n"); -} - -static void bitboard_print(bitboard b, FILE* out) -{ - setlocale(LC_ALL, ""); - fprintf(out, "\n"); - for (index i = 7; i < 8; i--) { - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "%"INDEX_FMT" ", i+1); - for (index j = 0; j < 8; ++j) { - index const n = INDEX_FROM_RF(i,j); - int color; - if ((b >> n) & 1) { - /* 41: red */ - color = 41; - } else { - /* 45: magenta, 47: white */ - color = (i+j)%2 ? 45 : 47; - } - fprintf(out, "\033[30;%dm", color); - fprintf(out, " "); - } - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "\n"); - } - fprintf(out, " A B C D E F G H \n"); -} - -static void board_print_threats(const struct pos* pos, FILE* out, struct move* move) -{ - enum player const player = pos->player; - int buf[8][8] = {0}; - - bitboard const threats = all_threats_from_player(pos, player); - - for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { - for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { - bitboard x = pos->pieces[player][piece]; - - for (index i = 7; i < 8; i--) { - for (index j = 0; j < 8; ++j) { - if (x & (1ULL<<(i*8+j))) { - buf[i][j] = piece_unicode[player][piece]; - } - } - } - } - } - - /* see: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */ - setlocale(LC_ALL, ""); /* needed for unicode symbols */ - for (index i = 7; i < 8; i--) { - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "%"INDEX_FMT" ", i+1); - for (index j = 0; j < 8; ++j) { - index const n = INDEX_FROM_RF(i,j); - - if (move && n == move->from) { - fprintf(out, "\033[%d;%dm", 30, 44); /* 44: blue*/ - } else if (move && n == move->to) { - fprintf(out, "\033[%d;%dm", 30, 44); /* 104: bright blue*/ - } - else if ((threats >> n) & 1) { - fprintf(out, "\033[%d;%dm", 30, 41); /* 41: red */ - } else { - /* 45: magenta, 47: white */ - fprintf(out, "\033[%d;%dm", 30, (i+j) % 2 ? 45 : 47); - } - if (buf[i][j]) { - fprintf(out, "%lc ", buf[i][j]); - } else { - fprintf(out, " "); /* idk why this hack is needed but "%lc " - is not working when buf[i][j] = ' ' */ - } - } - fprintf(out, "\033[0m"); /* reset background color */ - fprintf(out, "\n"); - } - fprintf(out, " A B C D E F G H \n"); -} diff --git a/board_print.h b/board_print.h new file mode 100644 index 0000000..97bd141 --- /dev/null +++ b/board_print.h @@ -0,0 +1,244 @@ + +#include +#include +#include +#include + +#define BITBOARD_FMT PRIu64 +#define BITBOARD_FMT_X PRIx64 + +#define INDEX_FMT PRIu8 + +static void bitboard_print(bitboard b, FILE* out) +{ + setlocale(LC_ALL, ""); + fprintf(out, "\n"); + for (index i = 7; i < 8; i--) { + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "%"INDEX_FMT" ", i+1); + for (index j = 0; j < 8; ++j) { + index const n = INDEX_FROM_RF(i,j); + int color; + if ((b >> n) & 1) { + /* 41: red */ + color = 41; + } else { + /* 45: magenta, 47: white */ + color = (i+j)%2 ? 45 : 47; + } + fprintf(out, "\033[30;%dm", color); + fprintf(out, " "); + } + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "\n"); + } + fprintf(out, " A B C D E F G H \n"); +} + +static void board_print_threats(const struct pos* pos, FILE* out, struct move* move) +{ + enum player const player = pos->player; + int buf[8][8] = {0}; + + bitboard const threats = all_threats_from_player(pos, player); + + for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { + for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { + bitboard x = pos->pieces[player][piece]; + + for (index i = 7; i < 8; i--) { + for (index j = 0; j < 8; ++j) { + if (x & (1ULL<<(i*8+j))) { + buf[i][j] = piece_unicode[player][piece]; + } + } + } + } + } + + /* see: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */ + setlocale(LC_ALL, ""); /* needed for unicode symbols */ + for (index i = 7; i < 8; i--) { + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "%"INDEX_FMT" ", i+1); + for (index j = 0; j < 8; ++j) { + index const n = INDEX_FROM_RF(i,j); + + if (move && n == move->from) { + fprintf(out, "\033[%d;%dm", 30, 44); /* 44: blue*/ + } else if (move && n == move->to) { + fprintf(out, "\033[%d;%dm", 30, 44); /* 104: bright blue*/ + } + else if ((threats >> n) & 1) { + fprintf(out, "\033[%d;%dm", 30, 41); /* 41: red */ + } else { + /* 45: magenta, 47: white */ + fprintf(out, "\033[%d;%dm", 30, (i+j) % 2 ? 45 : 47); + } + if (buf[i][j]) { + fprintf(out, "%lc ", buf[i][j]); + } else { + fprintf(out, " "); /* idk why this hack is needed but "%lc " + is not working when buf[i][j] = ' ' */ + } + } + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "\n"); + } + fprintf(out, " A B C D E F G H \n"); +} + +static void tt_print_stats(struct tt* tt, FILE* out) +{ +#ifndef NDEBUG + double sat = 0; + for (size_t i = 0; i < TT_ENTRIES; ++i) { + if (tt->entries[i].init) + sat += 1.0; + } + double const pct = sat/TT_ENTRIES; + + fprintf(out, "---- Stats ---\n"); + fprintf(out, "tt collisions: %"PRIu64"\n", tt->collisions); + fprintf(out, "tt hits: %"PRIu64"\n", tt->hits); + fprintf(out, "tt probes: %"PRIu64"\n", tt->probes); + fprintf(out, "tt insertions: %"PRIu64"\n", tt->insertions); + fprintf(out, "saturation: %.02lf\n", pct); +#else + fprintf(out, "stats not available with NDEBUG\n"); +#endif +} + +static void board_print_fen(struct pos const* pos, FILE* out) +{ + int buf[8][8] = {0}; + + for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { + for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { + bitboard x = pos->pieces[player][piece]; + + for (index i = 7; i < 8; i--) { + for (index j = 0; j < 8; ++j) { + if (x & (1ULL<<(i*8+j))) { + buf[i][j] = piece_char[player][piece]; + } + } + } + } + } + + for (enum rank_index i = RANK_INDEX_8; i < RANK_INDEX_COUNT; --i) { + int consequtive_empty = 0; + for (enum file_index j = FILE_INDEX_A; j < FILE_INDEX_COUNT; ++j) { + if (buf[i][j]) { + if (consequtive_empty) { + fprintf(out, "%d", consequtive_empty); + consequtive_empty = 0; + } + fprintf(out, "%lc", buf[i][j]); + } else { + consequtive_empty += 1; + } + } + if (consequtive_empty) { + fprintf(out, "%d", consequtive_empty); + } + if (i > 0) + fprintf(out, "/"); + } + + fprintf(out, " %c ", pos->player == PLAYER_WHITE ? 'w' : 'b'); + + bool any_castle = false; + if (!pos->castling_illegal[PLAYER_WHITE][CASTLE_KINGSIDE]) { + fprintf(out, "K"); + any_castle = true; + } + if (!pos->castling_illegal[PLAYER_WHITE][CASTLE_QUEENSIDE]) { + fprintf(out, "Q"); + any_castle = true; + } + if (!pos->castling_illegal[PLAYER_BLACK][CASTLE_KINGSIDE]) { + fprintf(out, "k"); + any_castle = true; + } + if (!pos->castling_illegal[PLAYER_BLACK][CASTLE_QUEENSIDE]) { + fprintf(out, "q"); + any_castle = true; + } + if (!any_castle) { + fprintf(out, "-"); + } + + if (pos->ep_targets) { + /* should be ep target square in algebraic notation */ + enum square_index const sqi = bitboard_lsb(pos->ep_targets); + + assert(sqi >= SQ_INDEX_BEGIN && sqi < SQ_INDEX_COUNT); + + enum file_index const fi = index_to_file(sqi); + enum rank_index const ri = index_to_rank(sqi); + + int const fch = tolower(file_index_char[fi]); + int const rch = tolower(rank_index_char[ri]); + + assert(fch >= 'a' && fch <= 'h'); + assert(rch >= '1' && rch <= '8'); + + fprintf(out, " %c%c", fch, rch); + } else { + fprintf(out, " -"); + } + + fprintf(out, " %d", pos->halfmoves); + fprintf(out, " %d", pos->fullmoves); + + fprintf(out, "\n"); +} + +static void board_print(const struct pos* pos, struct move* move, FILE* out) +{ + int buf[8][8] = {0}; + + for (enum player player = PLAYER_BEGIN; player < PLAYER_COUNT; ++player) { + for (enum piece piece = PIECE_BEGIN; piece < PIECE_COUNT; ++piece) { + bitboard x = pos->pieces[player][piece]; + + for (index i = 7; i < 8; i--) { + for (index j = 0; j < 8; ++j) { + if (x & (1ULL<<(i*8+j))) { + buf[i][j] = piece_unicode[player][piece]; + } + } + } + } + } + + /* see: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */ + setlocale(LC_ALL, ""); + for (index i = 7; i < 8; i--) { + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "%"INDEX_FMT" ", i+1); + for (index j = 0; j < 8; ++j) { + index const n = INDEX_FROM_RF(i,j); + if (move && n == move->from) { + fprintf(out, "\033[%d;%dm", 30, 104); /* 44: blue*/ + } else if (move && n == move->to) { + fprintf(out, "\033[%d;%dm", 30, 44); /* 104: bright blue*/ + } else { + /* 45: magenta, 47: white */ + fprintf(out, "\033[%d;%dm", 30, (i+j) % 2 ? 45 : 47); + } + if (buf[i][j]) { + fprintf(out, "%lc ", buf[i][j]); + } else { + fprintf(out, " "); /* idk why this hack is needed but "%lc " + is not working when buf[i][j] = ' ' */ + } + } + fprintf(out, "\033[0m"); /* reset background color */ + fprintf(out, "\n"); + } + fprintf(out, " A B C D E F G H \n"); +} + diff --git a/chess.html b/chess.html new file mode 100644 index 0000000..c31b45a --- /dev/null +++ b/chess.html @@ -0,0 +1,35 @@ + + + + + Wasm Loader + + + + + + +
+ + diff --git a/chess.js b/chess.js new file mode 100644 index 0000000..740669b --- /dev/null +++ b/chess.js @@ -0,0 +1,232 @@ + +const WasmHost = (() => { + async function load(url, imports = {}) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + const bytes = await res.arrayBuffer(); + const result = await WebAssembly.instantiate(bytes, imports); + const instance = result.instance ?? result; + return { instance, exports: instance.exports }; + } + + function getFn(exportsObj, name) { + const fn = exportsObj[name]; + if (typeof fn !== "function") { + throw new Error(`Export "${name}" is not a function`); + } + return fn; + } + + return { load, getFn }; +})(); + +/* +static inline struct move wasm_MoveTo_move(uint64_t m) +{ + return (struct move) { + .appeal = (m >> 24) & 0xFF, + .attr = (m >> 16) & 0xFF, + .from = (m >> 8) & 0xFF, + .to = (m >> 0) & 0xFF, + }; +} +*/ + +const MoveResult = Object.freeze({ + Normal: 0, + Check: 1, + Repeats: 2, + Stalemate: 3, + Checkmate: 4, +}); + +const _moveResultName = [ + "Normal", + "Check", + "Repeats", + "Stalemate", + "Checkmate", +]; + +const Player = Object.freeze({ + White: 0, + Black: 1, + None: 2, +}); + +const _playerName = [ + "White", + "Black", + "None", +]; + +const Piece = Object.freeze({ + Pawn: 0, + King: 1, + Queen: 2, + Bishop: 3, + Rook: 4, + Knight: 5, + Empty: 6, +}); + +const _pieceName = [ + "Pawn", + "King", + "Queen", + "Bishop", + "Rook", + "Knight", + "Empty", +]; + +const _pieceChar = [ + 'P', + 'K', + 'Q', + 'B', + 'R', + 'N', + ' ', +]; + +const _pieceUnicode = [ + ['♙', '♔', '♕', '♗', '♖', '♘',], // white + ['♟', '♚', '♛', '♝', '♜', '♞',], // black + ['' , '', '', '', '', '', ], // none +]; + +const indexDeserialize = (n) => ({ + rank: Math.floor(n / 8), // every day we stray further from God + file: (n % 8), + fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; }, + rankChar() { return ['1', '2', '3', '4', '5', '6', '7', '8'][this.rank]; }, +}); + + +const indexFromCoord = (rank, file) => (8*rank + file); + +const indexSerialize = (index) => (indexFromCoord(index.rank, index.file)); + +const moveDeserialize = (n) => ({ + appeal: Number((n >> 24n) & 0xFFn), + attr: Number((n >> 16n) & 0xFFn), + from: indexDeserialize(Number((n >> 8n) & 0xFFn)), + to: indexDeserialize(Number((n >> 0n) & 0xFFn)), +}); + +const moveSerialize = (move) => ((move.from & 0xFF) << 8) | ((move.to & 0xFF)); + +const squareDeserialize = (n) => ({ + player: n == -1 ? Player.None : (n >> 8) & 0xFF, + piece: n == -1 ? Piece.Empty : (n & 0xFF), + playerName() { + if (this.player === Player.None) { + return "Empty"; + } else { + return _playerName[this.player]; + } + }, + pieceName() { + if (this.player === Player.None) { + return ""; + } else { + return this.playerName() + _pieceName[this.piece]; + } + }, + pieceUnicode() { + return _pieceUnicode[this.player][this.piece]; + } +}); + + + +const run = async () => { + const { exports } = await WasmHost.load("./chess.wasm", {}); + + const wb_init = WasmHost.getFn(exports, "wb_init"); + const wb_move = WasmHost.getFn(exports, "wb_move"); + const wb_search = WasmHost.getFn(exports, "wb_search"); + const wb_board_at = WasmHost.getFn(exports, "wb_board_at"); + + wb_init(); + + const drawBoard = () => { + const board = document.getElementById("board"); + board.replaceChildren(); + const filesChars = ["a","b","c","d","e","f","g","h"]; + const ranksChars = ["8","7","6","5","4","3","2","1"]; + + for (let rank = 7; rank >= 0; --rank) { + for (let file = 0; file < 8; ++file) { + const square = document.createElement("div"); + square.className = + "square " + ((file + rank) % 2 ? "dark" : "light"); + square.dataset.file = filesChars[file]; + square.dataset.rank = ranksChars[rank]; + + const x = wb_board_at(indexFromCoord(rank, file)); + const sq = squareDeserialize(x); + square.textContent = sq.pieceUnicode(); + + board.appendChild(square); + } + } + setTimeout(() => { + // next task, browser had a chance to repaint + }, 0.01); + } + drawBoard(); + + const nextFrame = () => new Promise(requestAnimationFrame); + + for (let i = 0; i < 200; ++i) { + drawBoard(); + + await nextFrame(); + + const m = wb_search(7); + const move = moveDeserialize(m); + + console.log(move.from); + console.log(move.to); + + const fromSqEnc = wb_board_at(indexSerialize(move.from)); + const toSqEnc = wb_board_at(indexSerialize(move.to)); + + console.log(fromSqEnc); + console.log(toSqEnc); + + const fromSq = squareDeserialize(fromSqEnc); + const toSq = squareDeserialize(toSqEnc); + + console.log(fromSq) + console.log(toSq) + + console.log("from-player:", fromSq.playerName()); + console.log("from-piece:", fromSq.pieceName()); + + console.log("to-player:", toSq.playerName()); + console.log("to-piece:", toSq.pieceName()); + + console.log(m); + const f = move.from; + const t = move.to; + console.log("from:", move.from.fileChar(), move.from.rankChar(), + "to:", move.to.fileChar(), move.to.rankChar()); + + const mr = wb_move(m); + if (mr == MoveResult.Stalemate || mr == MoveResult.Checkmate) { + console.log(_moveResultName[mr]); + break; + } + } +} + +run().catch((err) => { + console.error(err); +}); + + diff --git a/evaluations.h b/evaluations.h new file mode 100644 index 0000000..3492f5c --- /dev/null +++ b/evaluations.h @@ -0,0 +1,202 @@ +/* TODO: candidate for code-generation */ + +#pragma once + +#define BITBOARD_WHITE( \ + a8,b8,c8,d8,e8,f8,g8,h8, \ + a7,b7,c7,d7,e7,f7,g7,h7, \ + a6,b6,c6,d6,e6,f6,g6,h6, \ + a5,b5,c5,d5,e5,f5,g5,h5, \ + a4,b4,c4,d4,e4,f4,g4,h4, \ + a3,b3,c3,d3,e3,f3,g3,h3, \ + a2,b2,c2,d2,e2,f2,g2,h2, \ + a1,b1,c1,d1,e1,f1,g1,h1) \ +(bitboard)\ +0b##\ +h8##g8##f8##e8##d8##c8##b8##a8##\ +h7##g7##f7##e7##d7##c7##b7##a7##\ +h6##g6##f6##e6##d6##c6##b6##a6##\ +h5##g5##f5##e5##d5##c5##b5##a5##\ +h4##g4##f4##e4##d4##c4##b4##a4##\ +h3##g3##f3##e3##d3##c3##b3##a3##\ +h2##g2##f2##e2##d2##c2##b2##a2##\ +h1##g1##f1##e1##d1##c1##b1##a1##\ +ULL + +#define BITBOARD_BLACK( \ + a8,b8,c8,d8,e8,f8,g8,h8, \ + a7,b7,c7,d7,e7,f7,g7,h7, \ + a6,b6,c6,d6,e6,f6,g6,h6, \ + a5,b5,c5,d5,e5,f5,g5,h5, \ + a4,b4,c4,d4,e4,f4,g4,h4, \ + a3,b3,c3,d3,e3,f3,g3,h3, \ + a2,b2,c2,d2,e2,f2,g2,h2, \ + a1,b1,c1,d1,e1,f1,g1,h1) \ +(bitboard)\ +0b##\ +h1##g1##f1##e1##d1##c1##b1##a1##\ +h2##g2##f2##e2##d2##c2##b2##a2##\ +h3##g3##f3##e3##d3##c3##b3##a3##\ +h4##g4##f4##e4##d4##c4##b4##a4##\ +h5##g5##f5##e5##d5##c5##b5##a5##\ +h6##g6##f6##e6##d6##c6##b6##a6##\ +h7##g7##f7##e7##d7##c7##b7##a7##\ +h8##g8##f8##e8##d8##c8##b8##a8##\ +ULL + +#define RELATIVE_DIAGONAL_A1_H8 \ + RELATIVE_BITBOARD( \ + 0,0,0,0,0,0,0,1, \ + 0,0,0,0,0,0,1,0, \ + 0,0,0,0,0,1,0,0, \ + 0,0,0,0,1,0,0,0, \ + 0,0,0,1,0,0,0,0, \ + 0,0,1,0,0,0,0,0, \ + 0,1,0,0,0,0,0,0, \ + 1,0,0,0,0,0,0,0) + +#define RELATIVE_DIAGONAL_A8_H1 \ + RELATIVE_BITBOARD( \ + 1,0,0,0,0,0,0,0, \ + 0,1,0,0,0,0,0,0, \ + 0,0,1,0,0,0,0,0, \ + 0,0,0,1,0,0,0,0, \ + 0,0,0,0,1,0,0,0, \ + 0,0,0,0,0,1,0,0, \ + 0,0,0,0,0,0,1,0, \ + 0,0,0,0,0,0,0,1) + +#define RELATIVE_BISHOP_KING_ATTACK \ + RELATIVE_BITBOARD( \ + 0,0,0,0,0,0,1,1, \ + 0,0,0,0,0,1,1,0, \ + 0,0,0,0,1,1,0,0, \ + 0,0,0,1,1,0,0,0, \ + 0,0,1,1,0,0,0,0, \ + 0,1,1,0,0,0,0,0, \ + 1,1,0,0,0,0,0,0, \ + 1,0,0,0,0,0,0,0) + +#define RELATIVE_BISHOP_QUEEN_ATTACK \ + RELATIVE_BITBOARD( \ + 1,1,0,0,0,0,0,0, \ + 0,1,1,0,0,0,0,0, \ + 0,0,1,1,0,0,0,0, \ + 0,0,0,1,1,0,0,0, \ + 0,0,0,0,1,1,0,0, \ + 0,0,0,0,0,1,1,0, \ + 0,0,0,0,0,0,1,1, \ + 0,0,0,0,0,0,0,1) + +#define RELATIVE_KING_CASTLE_KINGSIDE \ + RELATIVE_BITBOARD( \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,1,0) + +#define RELATIVE_KING_CASTLE_QUEENSIDE \ + RELATIVE_BITBOARD( \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,1,1,0,0,0,0,0) + +#define CORNERS \ + BITBOARD( \ + 1,0,0,0,0,0,0,1, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 1,0,0,0,0,0,0,1) + +#define RELATIVE_PAWN_SAFE_ZONE \ + BITBOARD( \ + 0,0,0,0,0,0,0,0, \ + 0,0,0,0,0,0,0,0, \ + 1,0,0,0,0,0,0,0, \ + 1,0,0,1,1,0,0,0, \ + 1,1,1,1,1,0,0,0, \ + 1,1,1,1,1,0,0,1, \ + 1,1,1,1,1,1,1,1, \ + 0,0,0,0,0,0,0,0) + +#define BOARD_CENTER_4X4 \ + ((FILE_MASK_C | FILE_MASK_D | FILE_MASK_E | FILE_MASK_F) \ + & (RANK_MASK_3 | RANK_MASK_4 | RANK_MASK_5 | RANK_MASK_6)) + +#define BOARD_CENTER_2X2 \ + ((FILE_MASK_D | FILE_MASK_E) \ + & (RANK_MASK_4 | RANK_MASK_5)) + + +#define POSITIONAL_BONUS_0 \ + /* piece bonus area*/ \ + X(PIECE_PAWN, 0.02, BOARD_CENTER_4X4) \ + X(PIECE_KNIGHT, 0.05, BOARD_CENTER_4X4) \ + X(PIECE_BISHOP, 0.05, RELATIVE_DIAGONAL_A1_H8 | RELATIVE_DIAGONAL_A8_H1) \ + X(PIECE_KING, 0.15, RELATIVE_KING_CASTLE_KINGSIDE) \ + X(PIECE_QUEEN, -0.10, RANK_MASK_3 | RANK_MASK_4 | RANK_MASK_5 | RANK_MASK_6) \ + /**/ + +#define POSITIONAL_BONUS_1 \ + /* piece bonus area*/ \ + X(PIECE_PAWN, 0.02, BOARD_CENTER_2X2) \ + X(PIECE_BISHOP, 0.05, RELATIVE_BISHOP_KING_ATTACK) \ + /**/ + +#define POSITIONAL_BONUS_2 \ + /* piece bonus area*/ \ + X(PIECE_PAWN, -0.10, ~RELATIVE_PAWN_SAFE_ZONE) \ + X(PIECE_BISHOP, 0.05, CORNERS) \ + /**/ + +#define POSITIONAL_BONUS_3 \ + /* piece bonus area*/ \ + X(PIECE_BISHOP, 0.02, RELATIVE_BISHOP_QUEEN_ATTACK) + +#define POSITIONAL_MODIFIER_COUNT 4 +static struct {bitboard const area; double const val;} const +positional_modifier[PLAYER_COUNT][POSITIONAL_MODIFIER_COUNT][PIECE_COUNT] = { +#define X(p, b, a) [p] = {.area = (a), .val = b}, +#define RELATIVE_BITBOARD BITBOARD_WHITE + [PLAYER_WHITE] = { + {POSITIONAL_BONUS_0}, + {POSITIONAL_BONUS_1}, + {POSITIONAL_BONUS_2}, + {POSITIONAL_BONUS_3}, + }, + [PLAYER_BLACK] = { +#undef RELATIVE_BITBOARD +#define RELATIVE_BITBOARD BITBOARD_BLACK + {POSITIONAL_BONUS_0}, + {POSITIONAL_BONUS_1}, + {POSITIONAL_BONUS_2}, + {POSITIONAL_BONUS_3}, + } +#undef X +}; + + +#undef CORNERS +#undef BOARD_CENTER_4X4 +#undef BOARD_CENTER_2X2 +#undef BITBOARD_WHITE +#undef BITBOARD_BLACK +#undef RELATIVE_DIAGONAL_A1_H8 +#undef RELATIVE_DIAGONAL_A8_H1 +#undef RELATIVE_BISHOP_KING_ATTACK +#undef RELATIVE_BISHOP_QUEEN_ATTACK +#undef RELATIVE_KING_CASTLE_KINGSIDE +#undef RELATIVE_KING_CASTLE_QUEENSIDE diff --git a/libc-lite.h b/libc-lite.h new file mode 100644 index 0000000..082847e --- /dev/null +++ b/libc-lite.h @@ -0,0 +1,80 @@ + +#include +#include +#include + +static uint64_t g_rand_seed = 1; + +void my_srand(uint64_t n) +{ + g_rand_seed = n == 0 ? 1 : n; +} + +uint64_t my_rand64() +{ + /* + * Xorshift RNG [1] + multiply + * [1]: https://www.jstatsoft.org/article/view/v008i14/916 */ + uint64_t x = g_rand_seed; + x ^= x >> 12ULL; + x ^= x << 25ULL; + x ^= x >> 27ULL; + g_rand_seed = x; + return x * 2685821657736338717ULL; +} + +static int my_tolower(int ch) +{ + return ch >= 'A' && ch <= 'Z' + ? ch - 'A' + 'a' + : ch; +} + +static int my_isdigit(int ch) +{ + return ch >= '0' && ch <= '9'; +} + +void* my_memset(void *b, int c, size_t len) +{ + unsigned char* x = b; + unsigned char ch = (unsigned char)c; + for (size_t i = 0; i < len; ++i) { + x[i] = ch; + } + return b; +} + +void* my_memcpy(void* restrict dst, void const* restrict src, size_t n) +{ + uint8_t* d = dst; + uint8_t const* s = src; + + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + } + + return dst; +} + +int my_memcmp(void const* s1, void const* s2, size_t n) +{ + /* todo: SIMD optimize or use 64 bit strides initially if optimizer + doesn't catch this */ + int8_t const* a = s1; + int8_t const* b = s2; + for (size_t i = 0; i < n; ++i) { + int8_t diff = a[i] - b[i]; + if (diff) + return diff; + } + return 0; +} + +size_t strlen(char const* s) +{ + size_t n = 0; + while (s[n++]) + ; + return n; +} diff --git a/sys.h b/sys.h new file mode 100644 index 0000000..8b543aa --- /dev/null +++ b/sys.h @@ -0,0 +1,194 @@ +#pragma once + + +#if defined(WASM) +#define PLATFORM_STR "Wasm" + +#include + +enum { /* TODO */ + SYS_PROT_NONE = 0, + SYS_PROT_READ = 0, + SYS_PROT_WRITE = 0, + SYS_PROT_EXEC = 0, + SYS_MADV_RANDOM = 0, + SYS_MADV_SEQUENTIAL = 0, +}; + +static uint8_t g_buf[1024*1024*1024]; +static size_t g_buf_len = 0; + +static void* sys_mmap_anon_shared(size_t size, int, int) +{ + /* FIXME: this program relies on few memory allocations, a simple bump + * allocator works for now, but will cause memory leaks in the future */ + + size = (size + 7ULL) & ~7ULL; + + if (g_buf_len + size > sizeof g_buf) { + return NULL; + } + void* x = &g_buf[g_buf_len]; + g_buf_len += size; + return x; +} + +#elif defined(__unix__) || defined(__unix) || \ + (defined(__APPLE__) && defined(__MACH__)) +#define PLATFORM_STR "Posix" + +#define _DARWIN_C_SOURCE 1 /* defines MAP_ANON */ +#include + +#define index string_index +#include +#undef index + +#include +#include + +enum { + SYS_PROT_NONE = PROT_NONE, + SYS_PROT_READ = PROT_READ, + SYS_PROT_WRITE = PROT_WRITE, + SYS_PROT_EXEC = PROT_EXEC, + + SYS_MADV_RANDOM = MADV_RANDOM, + SYS_MADV_SEQUENTIAL = MADV_SEQUENTIAL, +}; + +static void* sys_mmap_anon_shared(size_t size, int prot, int madv) +{ + void* x = mmap(NULL, size, prot, MAP_ANON | MAP_SHARED, -1, 0); + if (x == MAP_FAILED) { +#ifndef NDEBUG + fprintf(stderr, "%s (" PLATFORM_STR "): %s\n", __func__, strerror(errno)); +#endif + return NULL; + } + + /* mmap is de-facto zero filled on all platforms ^1, but zero it just in case + [1]: https://stackoverflow.com/a/65084762 */ + memset(x, 0, size); + + if (madv) { + int ok = madvise(x, size, madv); + if (ok == -1) { + fprintf(stderr, "%s (" PLATFORM_STR "): %s\n", __func__, strerror(errno)); + munmap(x, size); + return NULL; + } + } + + return x; +} + +#elif defined(_WIN32) +#define PLATFORM_STR "Win32" + +/* TODO: this is untested */ + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +enum { + SYS_PROT_NONE = 0, + SYS_PROT_READ = 1 << 0, + SYS_PROT_WRITE = 1 << 1, + SYS_PROT_EXEC = 1 << 2, +}; + +static DWORD sys_win_page_protect_from_prot(int prot) +{ + const int r = (prot & SYS_PROT_READ) != 0; + const int w = (prot & SYS_PROT_WRITE) != 0; + const int x = (prot & SYS_PROT_EXEC) != 0; + + if (!r && !w && !x) + return PAGE_NOACCESS; + + if (x) { + if (w) return PAGE_EXECUTE_READWRITE; + if (r) return PAGE_EXECUTE_READ; + return PAGE_EXECUTE; + } else { + if (w) return PAGE_READWRITE; /* no write-only pages on Windows */ + if (r) return PAGE_READONLY; + return PAGE_NOACCESS; + } +} + +static void sys_win_debug_print_last_error(const char* func_name) +{ +#ifdef NDEBUG + (void)func_name +#else + DWORD err = GetLastError(); + char* msg = NULL; + + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + err, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&msg, + 0, + NULL + ); + + if (msg) { + fprintf(stderr, "%s (" PLATFORM_STR "): %s", func_name, msg); + LocalFree(msg); + } else { + fprintf(stderr, "%s (" PLATFORM_STR "): GetLastError=%lu\n", + func_name, (unsigned long)err); + } +#endif +} + +static void* sys_mmap_anon_shared(size_t size, int prot) +{ + if (size == 0) + return NULL; + + DWORD protect = sys_win_page_protect_from_prot(prot); + + /* VirtualAlloc doesn't support PAGE_NOACCESS */ + DWORD alloc_protect = (protect == PAGE_NOACCESS) + ? PAGE_READWRITE + : protect; + + void* x = VirtualAlloc( + NULL, + size, + MEM_RESERVE | MEM_COMMIT, + alloc_protect + ); + + if (!x) { + sys_win_debug_print_last_error(__func__); + return NULL; + } + + if (protect == PAGE_NOACCESS) { + DWORD oldp; + if (!VirtualProtect(p, size, PAGE_NOACCESS, &oldp)) { + sys_win_debug_print_last_error(__func__); + VirtualFree(p, 0, MEM_RELEASE); + return NULL; + } + } + + return p; +} + +#else + +#error Unsupported platform + +#endif diff --git a/tests.c b/tests.c index 58a65b0..d23deb0 100644 --- a/tests.c +++ b/tests.c @@ -3,9 +3,11 @@ #include /* usleep */ #include "base.h" +#include "board_print.h" #include #include +#include /* Tests are mostly generated by ChatGPT */ @@ -388,9 +390,7 @@ static void test_bishops(void) int main() { - printf("sizeof board: %zu\n", sizeof (struct board)); printf("sizeof pos: %zu\n", sizeof (struct pos)); - printf("sizeof mailbox: %zu\n", sizeof (struct board){0}.mailbox); printf("sizeof tt: %zu\n", sizeof (struct tt)); #if 1 @@ -402,49 +402,60 @@ int main() fprintf(stdout, "\033[30;%dm ", i); } fprintf(stdout, "\033[0m\n"); /* reset background color */ - struct board board = BOARD_INITIAL; + + /* board is too big for the stack */ + struct board* b = malloc(sizeof *b); + if (!b) { + abort(); + } + *b = BOARD_INIT; //board_load_fen_unsafe(&board, "1n1q1rk1/r1p2P2/1p1pp2p/pB2P3/2P5/PPN5/6b1/3QK1NR b - - 0 1"); //board_load_fen_unsafe(&board, "5R2/7k/P7/6pp/3B4/1PPK2bP/4r3/8 b - - 3 57"); - board_print_fen(&board.pos, stdout); - board_print(&board.pos, NULL, stdout); + board_print_fen(&b->pos, stdout); + board_print(&b->pos, NULL, stdout); struct move moves[MOVE_MAX]; for (int turn = 0; turn < 200; ++turn) { size_t move_count = 0; - all_moves(&board.pos, board.pos.player, &move_count, moves); + all_moves(&b->pos, b->pos.player, &move_count, moves); if (move_count == 0) { - printf("no moves for %s, aborting\n", player_str[board.pos.player]); - board_print_threats(&board.pos, stdout, NULL); - board.pos.player = opposite_player(board.pos.player); - board_print_threats(&board.pos, stdout, NULL); + printf("no moves for %s, aborting\n", player_str[b->pos.player]); + board_print_threats(&b->pos, stdout, NULL); + b->pos.player = opposite_player(b->pos.player); + board_print_threats(&b->pos, stdout, NULL); break; } - + //struct move move = moves[0]; - struct move move = search(&board, board.pos.player, 8); + struct search_result sr = search(b, b->pos.player, 7); + + struct move move = sr.move; + double const score = sr.score; printf("move %d: {\n" " .from = %s, (%s)\n" " .to = %s,\n" + " .score = %lf,\n" " .mask = ", turn, square_index_display[move.from], - piece_str[board.mailbox[move.from]], - square_index_display[move.to] + piece_str[b->mailbox[move.from]], + square_index_display[move.to], + score ); if (move.attr & MATTR_CAPTURE) printf("MATTR_CAPTURE "); if (move.attr & MATTR_PROMOTE) printf("MATTR_PROMOTE "); printf("\n}\n"); - enum move_result const r = board_move_2(&board, move); + enum move_result const r = board_move_2(b, move); #if 1 - board_print_fen(&board.pos, stdout); - print_stats(stdout); - board_print(&board.pos, &move, stdout); + board_print_fen(&b->pos, stdout); + tt_print_stats(&b->tt, stdout); + board_print(&b->pos, &move, stdout); #endif if (r == MR_STALEMATE) { @@ -452,11 +463,11 @@ int main() break; } - if (board.pos.pieces[PLAYER_WHITE][PIECE_KING] == 0ULL) { + if (b->pos.pieces[PLAYER_WHITE][PIECE_KING] == 0ULL) { printf("white king gone!!\n"); exit(1); } - if (board.pos.pieces[PLAYER_BLACK][PIECE_KING] == 0ULL) { + if (b->pos.pieces[PLAYER_BLACK][PIECE_KING] == 0ULL) { printf("black king gone!!\n"); exit(1); } diff --git a/wasm-compat.c b/wasm-compat.c new file mode 100644 index 0000000..110bf1b --- /dev/null +++ b/wasm-compat.c @@ -0,0 +1,71 @@ + +#include "base.h" + +#include + +static struct search_option g_tt_buf[TT_ENTRIES]; + +static struct board g_board; + +static inline uint32_t move_serialize(struct move m) +{ + _Static_assert(sizeof m.from * CHAR_BIT == 8, + "this must be checked if struct move's `from` changes"); + _Static_assert(sizeof m.to * CHAR_BIT == 8, + "this must be checked if struct move's `to` changes"); + return ((uint32_t)m.appeal << 24ULL) + | ((uint32_t)m.attr << 16ULL) + | ((uint32_t)m.from << 8ULL) + | ((uint32_t)m.to); +} + +static inline struct move move_deserialize(uint64_t m) +{ + return (struct move) { + /* appeal and attributes are ignored regardless */ + /* + .appeal = (m >> 24) & 0xFF, + .attr = (m >> 16) & 0xFF, + */ + .from = (m >> 8) & 0xFF, + .to = (m >> 0) & 0xFF, + }; +} + +uint64_t wb_search(int8_t max_depth) +{ + struct search_result const sr = search(&g_board, g_board.pos.player, max_depth); + return move_serialize(sr.move); +} + +enum move_result wb_move(uint32_t move) +{ + struct move m = move_deserialize(move); + enum move_result const r = board_move_2(&g_board, m); + return r; +} + +void wb_init() +{ + g_board = BOARD_INIT; + g_board.tt.entries = g_tt_buf; +} + +static int32_t player_piece_serialize(enum player c, enum piece pz) +{ + return ((c & 0xFF) << 8U) + | (pz & 0xFF); +} + +int32_t wb_board_at(index at) +{ + bitboard const m = SQ_MASK_FROM_INDEX(at); + for (enum player pl = PLAYER_BEGIN; pl < PLAYER_COUNT; ++pl) { + for (enum piece pz = PIECE_BEGIN; pz < PIECE_COUNT; ++pz) { + if (g_board.pos.pieces[pl][pz] & m) { + return player_piece_serialize(pl, pz); + } + } + } + return -1; +}