From 5a5a392c8ba47a3f6b1dd8c50e8b092150f93d8b Mon Sep 17 00:00:00 2001 From: Ole Morud Date: Wed, 7 Jan 2026 15:15:18 +0100 Subject: [PATCH] Performance improvements --- Makefile | 9 +- board_print.h | 7 +- engine-attack-sets.h | 19 ++++ engine-board.h | 96 ++++++++++++++------ engine-evaluations.h | 58 ++++++------ engine-macros.h | 5 ++ engine-move-generation.h | 1 - engine-tt.h | 29 +++--- engine-types.h | 55 +++++++++--- engine.h | 190 +++++++++++++++++++++++++-------------- tests.c | 45 ++++++---- wasm-compat.c | 90 +++++++++---------- 12 files changed, 385 insertions(+), 219 deletions(-) diff --git a/Makefile b/Makefile index 9f5e18a..8881f59 100644 --- a/Makefile +++ b/Makefile @@ -4,13 +4,14 @@ CC := clang CFLAGS.gcc := -std=c23 -Wall -Wextra -Wconversion -Wno-unused-function CFLAGS.gcc.release := -Ofast -march=native -DNDEBUG -CFLAGS.gcc.debug := -ggdb -O0 -fsanitize=address +CFLAGS.gcc.debug := -ggdb -O1 -fsanitize=address CFLAGS.clang := -std=c23 -g -Wall -Wextra -Wconversion -Wno-unused-function -Wimplicit-int-conversion -CFLAGS.clang.release := -O3 -ffast-math -march=native # -DNDEBUG -CFLAGS.clang.debug := -g3 -O0 -fsanitize=address +CFLAGS.clang.release := -O3 -ffast-math -march=native -DNDEBUG -DNSTATS +CFLAGS.clang.debug := -g3 -O1 -fsanitize=address,undefined CFLAGS.clang.wasm := \ --target=wasm32-unknown-unknown -nostdlib -g \ + -DNSTATS \ -Wl,--export-all \ -Wl,--no-entry @@ -33,4 +34,4 @@ mbb_bishop.h: codegen ./codegen tests: tests.c mbb_rook.h mbb_bishop.h engine.h - $(CC) -o $@ $(CFLAGS) -DUSE_PRINTF tests.c + $(CC) -o $@ $(CFLAGS) tests.c diff --git a/board_print.h b/board_print.h index c5cf8a8..e7944cf 100644 --- a/board_print.h +++ b/board_print.h @@ -38,13 +38,12 @@ static void bitboard_print(Bb64 b, FILE* out) 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; + double const pct = sat/((double)tt->mask+1.0); fprintf(out, "---- Stats ---\n"); fprintf(out, "tt collisions: %"PRIu64"\n", tt->collisions); @@ -52,10 +51,6 @@ static void tt_print_stats(struct tt* tt, FILE* out) fprintf(out, "tt probes: %"PRIu64"\n", tt->probes); fprintf(out, "tt insertions: %"PRIu64"\n", tt->insertions); fprintf(out, "saturation: %.02lf\n", pct); -#else - (void)tt; - fprintf(out, "stats not available with NDEBUG\n"); -#endif } static void board_print_fen(struct pos const* pos, FILE* out) diff --git a/engine-attack-sets.h b/engine-attack-sets.h index 55119c9..1750aaa 100644 --- a/engine-attack-sets.h +++ b/engine-attack-sets.h @@ -15,6 +15,16 @@ static Bb64 cardinals_from_index(Sq8 p) return (FILE_MASK(sq_to_file(p)) | RANK_MASK(sq_to_rank(p))); } +static Bb64 cardinals(Bb64 p) +{ + Bb64 b = 0ULL; + while (p) { + Sq8 const lsb = bitboard_pop_lsb(&p); + b |= cardinals_from_index(lsb); + } + return b; +} + static Bb64 diagonals_from_index(Sq8 sq) { #ifdef CODEGEN @@ -66,6 +76,15 @@ static Bb64 diagonals_from_index(Sq8 sq) #endif } +static Bb64 diagonals(Bb64 p) +{ + Bb64 b = 0ULL; + while (p) { + Sq8 const lsb = bitboard_pop_lsb(&p); + b |= diagonals_from_index(lsb); + } + return b; +} /* PIECE ATTACKS diff --git a/engine-board.h b/engine-board.h index ed1d29c..6ed3d29 100644 --- a/engine-board.h +++ b/engine-board.h @@ -23,7 +23,7 @@ struct board { /* used for repeated board state detection only */ struct history { - struct pos items[64]; + uint64_t hashes[50]; /* array of zobrist hashes */ size_t length; } hist; @@ -36,6 +36,9 @@ struct board { Piece8 mailbox[SQ_COUNT]; }; +/* attack-sets depends on the definition of struct pos, whoops ¯\_(ツ)_/¯ */ +#include "engine-attack-sets.h" + #define BOARD_INIT (struct board) { \ .pos = { \ .fullmoves = 1, \ @@ -112,9 +115,12 @@ void board_init(struct board* b) *b = BOARD_INIT; } if (b->tt.entries == NULL) { - b->tt.entries = sys_mmap_anon_shared(TT_ENTRIES * sizeof b->tt.entries[0], + size_t const entries = 1<<26; + size_t const mask = entries-1; + b->tt.entries = sys_mmap_anon_shared(entries * sizeof b->tt.entries[0], SYS_PROT_READ | SYS_PROT_WRITE, SYS_MADV_RANDOM); + b->tt.mask = mask; if (b->tt.entries == NULL) { __builtin_trap(); } @@ -232,13 +238,23 @@ enum move_result { MR_CHECKMATE, }; +struct move_undo { + Piece8 captured_piece; + Piece8 moved_piece; + Sq8 captured_square; + Sq8 moved_square; +}; + /* does not check validity */ static enum move_result move_piece(struct pos* restrict pos, + Side8 us, struct history* restrict hist, Piece8 mailbox[restrict static SQ_COUNT], struct move move) { - Side8 const us = pos->moving_side; + struct move_undo undo; + + //Side8 const us = pos->moving_side; Side8 const them = other_side(us); Piece8 const from_piece = mailbox[move.from]; @@ -375,36 +391,42 @@ static enum move_result move_piece(struct pos* restrict pos, pos->fullmoves += (pos->moving_side == SIDE_BLACK); pos->halfmoves += 1; - assuming(hist->length < 64); - int repetitions = 0; - for (size_t i = 0; i < hist->length; ++i) { - _Static_assert(sizeof *pos == sizeof hist->items[i]); - 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].moving_side == pos->moving_side - && hist->items[i].ep_targets == pos->ep_targets) - { - repetitions += 1; + assuming(hist->length < sizeof hist->hashes / sizeof *hist->hashes); + + /* check for repeated moves */ + /* TODO: add move_do and move_undo to create proper repeat checks */ + { + size_t i; + for (i = 0; i < hist->length / 8; ++i) { + uint64_t vec_a[8]; + uint64_t vec_b[8]; + bool match = false; + for (size_t vec_i = 0; vec_i < 8; ++vec_i) { + vec_a[i] = pos->hash; + } + for (size_t vec_i = 0; vec_i < 8; ++vec_i) { + vec_b[i] = hist->hashes[8*i + vec_i]; + } + for (size_t vec_i = 0; vec_i < 8; ++vec_i) { + if (vec_a[vec_i] == vec_b[vec_i]) { + match = true; + } + } + if (match) { + return MR_STALEMATE; + } } + for (; i < hist->length; ++i) { + if (hist->hashes[i] == pos->hash) { + return MR_STALEMATE; + } + } + hist->hashes[hist->length++] = pos->hash; } - hist->items[hist->length++] = *pos; - - if (repetitions >= 3 || pos->halfmoves > 50) { + if (pos->halfmoves > 50) { return MR_STALEMATE; } - else if (repetitions > 0) { - return MR_REPEATS; - } - else if (pos->halfmoves > 50) { - return MR_STALEMATE; - } -#if 0 - else if (attacks_to(pos, pos->pieces[them][PIECE_KING], 0ULL, 0ULL) - & ~pos->occupied[them]) { - return MR_CHECK; - } -#endif else { return MR_NORMAL; } @@ -417,7 +439,25 @@ static enum move_result move_piece(struct pos* restrict pos, static enum move_result board_move(struct board* b, struct move move) { return move_piece(&b->pos, + b->pos.moving_side, &b->hist, b->mailbox, move); } + +static bool pos_is_legal(struct pos const* restrict pos) +{ + Side8 const mside = pos->moving_side; + Side8 const oside = other_side(mside); + + if (pos->pieces[oside][PIECE_KING] & all_threats_from_side(pos, mside)) { + return false; + } + + return true; +} + +static bool board_is_legal(struct board* b) +{ + return pos_is_legal(&b->pos); +} diff --git a/engine-evaluations.h b/engine-evaluations.h index 79e7a2f..41c4b46 100644 --- a/engine-evaluations.h +++ b/engine-evaluations.h @@ -314,25 +314,25 @@ BITBOARD( \ #define EARLY_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, BOARD_CENTER_6X6 & (DIAGONAL_A1_H8 | DIAGONAL_A8_H1)) \ - X(PIECE_KING, 0.15, REL_KING_CASTLE_KINGSIDE) \ - X(PIECE_QUEEN, -0.15, RANK_MASK_3 | RANK_MASK_4 | RANK_MASK_5 | RANK_MASK_6) \ - X(PIECE_ROOK, 0.10, FILE_MASK_D | FILE_MASK_E) \ + X(PIECE_PAWN, 0.02f, BOARD_CENTER_4X4) \ + X(PIECE_KNIGHT, 0.05f, BOARD_CENTER_4X4) \ + X(PIECE_BISHOP, 0.05f, BOARD_CENTER_6X6 & (DIAGONAL_A1_H8 | DIAGONAL_A8_H1)) \ + X(PIECE_KING, 0.15f, REL_KING_CASTLE_KINGSIDE) \ + X(PIECE_QUEEN, -0.15f, RANK_MASK_3 | RANK_MASK_4 | RANK_MASK_5 | RANK_MASK_6) \ + X(PIECE_ROOK, 0.10f, FILE_MASK_D | FILE_MASK_E) \ /**/ #define EARLY_POSITIONAL_BONUS_1 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.02, BOARD_CENTER_2X2) \ - X(PIECE_BISHOP, 0.05, REL_BISHOP_KING_ATTACK) \ + X(PIECE_PAWN, 0.02f, BOARD_CENTER_2X2) \ + X(PIECE_BISHOP, 0.05f, REL_BISHOP_KING_ATTACK) \ /**/ #define EARLY_POSITIONAL_BONUS_2 \ /* piece bonus area*/ \ - X(PIECE_PAWN, -0.18, ~REL_EARLY_PAWN_STRUCTURE) \ - X(PIECE_KNIGHT, -0.10, REL_UNDEVELOPED_KNIGHTS) \ - X(PIECE_BISHOP, -0.10, REL_UNDEVELOPED_BISHOPS) \ + X(PIECE_PAWN, -0.18f, ~REL_EARLY_PAWN_STRUCTURE) \ + X(PIECE_KNIGHT, -0.10f, REL_UNDEVELOPED_KNIGHTS) \ + X(PIECE_BISHOP, -0.10f, REL_UNDEVELOPED_BISHOPS) \ /**/ #define EARLY_POSITIONAL_BONUS_3 \ @@ -345,58 +345,58 @@ BITBOARD( \ #define MIDDLE_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, BOARD_CENTER_6X6 & (DIAGONAL_A1_H8 | DIAGONAL_A8_H1)) \ - X(PIECE_KING, 0.15, REL_KING_CASTLE_KINGSIDE) \ + X(PIECE_PAWN, 0.02f, BOARD_CENTER_4X4) \ + X(PIECE_KNIGHT, 0.05f, BOARD_CENTER_4X4) \ + X(PIECE_BISHOP, 0.05f, BOARD_CENTER_6X6 & (DIAGONAL_A1_H8 | DIAGONAL_A8_H1)) \ + X(PIECE_KING, 0.15f, REL_KING_CASTLE_KINGSIDE) \ /**/ #define MIDDLE_POSITIONAL_BONUS_1 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.02, BOARD_CENTER_2X2) \ - X(PIECE_BISHOP, 0.07, REL_BISHOP_KING_ATTACK) \ - X(PIECE_QUEEN, 0.07, REL_BISHOP_KING_ATTACK) \ + X(PIECE_PAWN, 0.02f, BOARD_CENTER_2X2) \ + X(PIECE_BISHOP, 0.07f, REL_BISHOP_KING_ATTACK) \ + X(PIECE_QUEEN, 0.07f, REL_BISHOP_KING_ATTACK) \ /**/ #define MIDDLE_POSITIONAL_BONUS_2 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.02, REL_PAWN_KINGSIDE) \ + X(PIECE_PAWN, 0.02f, REL_PAWN_KINGSIDE) \ /**/ #define MIDDLE_POSITIONAL_BONUS_3 \ /* piece bonus area*/ \ - X(PIECE_BISHOP, 0.05, BOARD_CENTER_6X6) \ - X(PIECE_KNIGHT, 0.05, BOARD_CENTER_6X6) \ + X(PIECE_BISHOP, 0.05f, BOARD_CENTER_6X6) \ + X(PIECE_KNIGHT, 0.05f, BOARD_CENTER_6X6) \ /**/ /* ------------------------------- end game -------------------------------- */ #define LATE_POSITIONAL_BONUS_0 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.30, REL_RANK_7 | REL_RANK_6 | REL_RANK_5) \ - X(PIECE_KING, 0.10, BOARD_CENTER_6X6) \ + X(PIECE_PAWN, 0.30f, REL_RANK_7 | REL_RANK_6 | REL_RANK_5) \ + X(PIECE_KING, 0.10f, BOARD_CENTER_6X6) \ /**/ #define LATE_POSITIONAL_BONUS_1 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.30, REL_RANK_7 | REL_RANK_6) \ - X(PIECE_KING, 0.10, BOARD_CENTER_4X4) \ + X(PIECE_PAWN, 0.30f, REL_RANK_7 | REL_RANK_6) \ + X(PIECE_KING, 0.10f, BOARD_CENTER_4X4) \ /**/ #define LATE_POSITIONAL_BONUS_2 \ /* piece bonus area*/ \ - X(PIECE_PAWN, 0.70, REL_RANK_7) \ - X(PIECE_KING, 0.10, BOARD_CENTER_2X2) \ + X(PIECE_PAWN, 0.70f, REL_RANK_7) \ + X(PIECE_KING, 0.10f, BOARD_CENTER_2X2) \ /**/ #define LATE_POSITIONAL_BONUS_3 \ /* piece bonus area*/ \ - X(PIECE_KING, -0.50, ~BOARD_CENTER_6X6) \ + X(PIECE_KING, -0.50f, ~BOARD_CENTER_6X6) \ /**/ struct posmod { Bb64 const area; - double const val; + float const val; }; static inline struct posmod positional_modifier(Side8 pl, enum game_progress st, size_t layer, Piece8 pz) diff --git a/engine-macros.h b/engine-macros.h index 83a1527..028d5f0 100644 --- a/engine-macros.h +++ b/engine-macros.h @@ -11,3 +11,8 @@ #else #define assuming(expr) ((expr) ? 0 : (__builtin_unreachable(), 0)) #endif + +#define CACHE_LINE_SIZE 64 + +#define LIKELY(expr) __builtin_expect((expr), 1) +#define UNLIKELY(expr) __builtin_expect((expr), 0) diff --git a/engine-move-generation.h b/engine-move-generation.h index c043556..d7d1185 100644 --- a/engine-move-generation.h +++ b/engine-move-generation.h @@ -178,7 +178,6 @@ static void all_pseudolegal_moves(struct pos const* restrict pos, Bb64 const their_threats = all_threats_from_side(pos, them); Bb64 const their_occ = pos->occupied[them]; - Bb64 const all_occ = pos->occupied[SIDE_WHITE] | pos->occupied[SIDE_BLACK]; Bb64 allowed; if (type == MG_CAPTURES) { diff --git a/engine-tt.h b/engine-tt.h index 5690fe8..2af92b5 100644 --- a/engine-tt.h +++ b/engine-tt.h @@ -1,28 +1,27 @@ #pragma once struct search_option { - /* TODO: optimize order of fields and size */ - double score; - struct move move; - uint64_t hash; - uint8_t init : 1; - enum tt_flag {TT_EXACT, TT_LOWER, TT_UPPER} flag : 3; - int8_t depth : 4; + uint64_t hash; // + 64 (64 / 8) + float score; // + 32 (96 / 12) + struct move move; // + 32 (128 / 16) + int depth : 4; // + 4 (132 / 17) + int init : 1; // + 1 (133 / 17) + enum tt_flag {TT_EXACT, TT_LOWER, TT_UPPER} flag : 3; // +3 (136 / 17) }; #define TT_ADDRESS_BITS 24 #define TT_ENTRIES (1ULL<mask; struct search_option tte = tt->entries[addr]; -#ifndef NDEBUG +#ifndef NSTATS tt->probes += 1; if (tte.init && tte.hash == hash) { tt->hits += 1; @@ -108,10 +107,10 @@ static inline struct search_option tt_get(struct tt* tt, uint64_t hash) static inline void tt_insert(struct tt* tt, uint64_t hash, struct search_option so) { - uint64_t const addr = hash % TT_ENTRIES; + uint64_t const addr = hash & tt->mask; so.init = true; tt->entries[addr] = so; -#ifndef NDEBUG +#ifndef NSTATS tt->insertions += 1; #endif } @@ -123,7 +122,7 @@ static inline void tt_insert(struct tt* tt, uint64_t hash, struct search_option */ static inline void tt_insert_maybe(struct tt* tt, uint64_t hash, struct search_option so) { - uint64_t const addr = hash % TT_ENTRIES; + uint64_t const addr = hash & tt->mask; #if 0 struct search_option* tte = &tt->entries[addr]; @@ -134,7 +133,7 @@ static inline void tt_insert_maybe(struct tt* tt, uint64_t hash, struct search_o so.init = true; tt->entries[addr] = so; -#ifndef NDEBUG +#ifndef NSTATS tt->insertions += 1; #endif } diff --git a/engine-types.h b/engine-types.h index b37aa2a..3174dd2 100644 --- a/engine-types.h +++ b/engine-types.h @@ -140,6 +140,7 @@ typedef enum sq8 : uint8_t { #define X(file, rank) SQ_##file##rank = SQ_FROM_RF(RANK_INDEX_##rank, FILE_INDEX_##file), SQUARES_LIST SQ_COUNT, + SQ_POISONED, #undef X /* define iterator begin enum */ #define X(file, rank) SQ_BEGIN = SQ_##file##rank, @@ -192,24 +193,26 @@ static char const* side_str[SIDE_COUNT] = { /* ----------- Piece8 ----------- */ /* https://en.wikipedia.org/wiki/X_macro */ -/* enum value white char white unicode black char black unicode */ +/* enum value white char white unicode black char black unicode */ #define PIECES \ - X(PIECE_PAWN, 1.0, 'P', 0x2659, 'p', 0x265F) \ - X(PIECE_KING, 0.0, 'K', 0x2654, 'k', 0x265A) \ - X(PIECE_QUEEN, 9.0, 'Q', 0x2655, 'q', 0x265B) \ - X(PIECE_BISHOP, 3.0, 'B', 0x2657, 'b', 0x265D) \ - X(PIECE_ROOK, 5.0, 'R', 0x2656, 'r', 0x265C) \ - X(PIECE_KNIGHT, 3.0, 'N', 0x2658, 'n', 0x265E) + X(PIECE_PAWN, 1.0f, 'P', 0x2659, 'p', 0x265F) \ + X(PIECE_KNIGHT, 3.1f, 'N', 0x2658, 'n', 0x265E) \ + X(PIECE_BISHOP, 3.2f, 'B', 0x2657, 'b', 0x265D) \ + X(PIECE_ROOK, 5.0f, 'R', 0x2656, 'r', 0x265C) \ + X(PIECE_QUEEN, 9.0f, 'Q', 0x2655, 'q', 0x265B) \ + X(PIECE_KING, 16.0f, 'K', 0x2654, 'k', 0x265A) \ + /**/ typedef enum piece : uint8_t { #define X(e, v, wc, wu, bc, bu) e, PIECES PIECE_COUNT, PIECE_BEGIN = 0, + PIECE_POISONED, /* used as default undefined value in debug builds */ #undef X } Piece8; -static double piece_value[PIECE_COUNT] = { +static float piece_value[PIECE_COUNT] = { #define X(e, v, wc, wu, bc, bu) [e] = v, PIECES #undef X @@ -260,6 +263,7 @@ static int const piece_unicode[SIDE_COUNT][PIECE_COUNT] = { enum { MATTR_PROMOTE = 1<<0, + MATTR_POISONED = 1<<1, }; struct move { @@ -272,9 +276,11 @@ struct move { _Static_assert(sizeof(struct move) == 4, "this static assuming is here to check when sizeof(move) changes"); -#define NULL_MOVE (struct move){0} +#define MOVE_NULL (struct move){0} -#define IS_NULL_MOVE(m) ((m).from == (m).to) +#define MOVE_POISONED (struct move) {.from = SQ_POISONED, .to = SQ_POISONED, .attr = MATTR_POISONED } + +#define IS_MOVE_NULL(m) ((m).from == (m).to) /* ----------- castle_direction ----------- */ enum castle_direction { @@ -283,3 +289,32 @@ enum castle_direction { CASTLE_QUEENSIDE, CASTLE_COUNT, }; + + +/* -------------- stop flag --------------- */ + +/* occupy an entire cache line to avoid invalidation from neighboring writes */ +struct searching_flag { + alignas(CACHE_LINE_SIZE) atomic_uint_fast8_t v; + uint8_t pad[CACHE_LINE_SIZE - sizeof(atomic_uint_fast8_t)]; +}; + +static inline void searching_init(struct searching_flag* restrict sf) +{ + atomic_init(&sf->v, 0); +} + +static inline bool searching_still(struct searching_flag const* restrict sf) +{ + return atomic_load_explicit(&sf->v, memory_order_relaxed); +} + +static inline void searching_start(struct searching_flag* restrict sf) +{ + atomic_store_explicit(&sf->v, 1, memory_order_relaxed); +} + +static inline void searching_stop(struct searching_flag* restrict sf) +{ + atomic_store_explicit(&sf->v, 0, memory_order_relaxed); +} diff --git a/engine.h b/engine.h index 1262793..7c42aec 100644 --- a/engine.h +++ b/engine.h @@ -1,5 +1,8 @@ #pragma once +/* WIP switches */ +#define NEW_SEARCHING + #include "libc-lite.h" #include "sys.h" @@ -20,8 +23,8 @@ static uint64_t g_ab_node_volume = 0; /* --------------------------- MOVE SEARCH --------------------------------- */ -#define SCORE_INF 1e30 -#define SCORE_CHECKMATE 999.0 +#define SCORE_INF 1e10f +#define SCORE_CHECKMATE 999.0f /* for initial ordering of moves in alphabeta search */ static void move_compute_appeal(struct move* restrict m, @@ -38,16 +41,16 @@ static void move_compute_appeal(struct move* restrict m, n += (uint8_t)piece_value[mailbox[m->to]]; } - uint8_t mmv_lva_bonus = 16*n - (uint8_t)piece_value[atk]; + uint8_t mmv_lva_bonus = (uint8_t)(16.0f*(float)n - piece_value[atk]); m->appeal = mmv_lva_bonus; } -static double board_score_heuristic(struct pos const* pos) +static float board_score_heuristic(struct pos const* pos) { /* this function always evaluates from white's perspective before eventually flipping the sign based on `pos` */ - double score = 0.0; + float score = 0.0f; Bb64 const occw = pos->occupied[SIDE_WHITE]; Bb64 const occb = pos->occupied[SIDE_BLACK]; @@ -55,57 +58,67 @@ static double board_score_heuristic(struct pos const* pos) enum game_progress const gp = endgameness(pos); - if (pos->pieces[SIDE_WHITE][PIECE_KING] == 0) score -= (double)SCORE_CHECKMATE; - if (pos->pieces[SIDE_BLACK][PIECE_KING] == 0) score += (double)SCORE_CHECKMATE; + if (pos->pieces[SIDE_WHITE][PIECE_KING] == 0) { + score -= SCORE_CHECKMATE; + goto end; + } + if (pos->pieces[SIDE_BLACK][PIECE_KING] == 0) { + score += SCORE_CHECKMATE; + goto end; + } for (Piece8 p = PIECE_BEGIN; p < PIECE_COUNT; ++p) { /* raw material value */ score += piece_value[p] * - ((double)bitboard_popcount(pos->pieces[SIDE_WHITE][p]) - - (double)bitboard_popcount(pos->pieces[SIDE_BLACK][p])); + ((float)bitboard_popcount(pos->pieces[SIDE_WHITE][p]) - + (float)bitboard_popcount(pos->pieces[SIDE_BLACK][p])); /* very minor advantage for threat projection to break tied moves */ - if (p != PIECE_PAWN) { - score += 0.001 * ( - (double)bitboard_popcount(non_pawn_piece_attacks(p, pos->pieces[SIDE_WHITE][p], occall)) - - (double)bitboard_popcount(non_pawn_piece_attacks(p, pos->pieces[SIDE_BLACK][p], occall))); - } + //if (p != PIECE_PAWN) { + // score += 0.001f * ( + // (float)bitboard_popcount(non_pawn_piece_attacks(p, pos->pieces[SIDE_WHITE][p], occall)) + // - (float)bitboard_popcount(non_pawn_piece_attacks(p, pos->pieces[SIDE_BLACK][p], occall))); + //} /* positional bonus, see evaluations.h */ for (size_t i = 0; i < POSITIONAL_MODIFIER_COUNT; ++i) { score += positional_modifier(SIDE_WHITE, gp, i, p).val * ( - (double)bitboard_popcount( + (float)bitboard_popcount( pos->pieces[SIDE_WHITE][p] & positional_modifier(SIDE_WHITE, gp, i, p).area) - - (double)bitboard_popcount( + - (float)bitboard_popcount( pos->pieces[SIDE_BLACK][p] & positional_modifier(SIDE_BLACK, gp, i, p).area) ); } } + score += 0.10f * (float)bitboard_more_than_one(pos->pieces[SIDE_WHITE][PIECE_BISHOP]); + score -= 0.10f * (float)bitboard_more_than_one(pos->pieces[SIDE_BLACK][PIECE_BISHOP]); + /* pawns defending pieces are desired */ - score += 0.03 * ( - (double)bitboard_popcount( + score += 0.03f * ( + (float)bitboard_popcount( pawn_attacks_white(pos->pieces[SIDE_WHITE][PIECE_PAWN]) & occw ) - - (double)bitboard_popcount( + - (float)bitboard_popcount( pawn_attacks_black(pos->pieces[SIDE_BLACK][PIECE_PAWN]) & occb ) ); /* stacked pawns are bad */ - const double k = 0.30; + const float k = 0.30f; for (enum file_index fi = FILE_INDEX_BEGIN; fi < FILE_INDEX_COUNT; ++fi) { uint64_t wstk = bitboard_popcount(pos->pieces[SIDE_WHITE][PIECE_PAWN] & FILE_MASK(fi)); uint64_t bstk = bitboard_popcount(pos->pieces[SIDE_BLACK][PIECE_PAWN] & FILE_MASK(fi)); - score -= k * (double)(wstk - (wstk == 1)); - score += k * (double)(bstk - (bstk == 1)); + score -= k * (float)(wstk - (wstk == 1)); + score += k * (float)(bstk - (bstk == 1)); } - double sign = (pos->moving_side == SIDE_WHITE) ? 1.0 : -1.0; +end: + float sign = (pos->moving_side == SIDE_WHITE) ? 1.0f : -1.0f; return sign*score; } @@ -131,21 +144,21 @@ struct move moves_linear_search(struct move moves[restrict static MOVE_MAX] /* quiescence is a deep search that only considers captures */ static -double quiesce(struct pos const* pos, +float quiesce(struct pos const* pos, Piece8 mailbox[restrict static SQ_COUNT], Side8 us, - double alpha, - double beta, + float alpha, + float beta, int8_t depth) { if (pos->pieces[us][PIECE_KING] == 0) { - return -SCORE_CHECKMATE; + return -(SCORE_CHECKMATE + (float)depth); } Side8 const them = other_side(us); - double score = board_score_heuristic(pos); - double highscore = score; + float score = board_score_heuristic(pos); + float highscore = score; if (highscore >= beta) { return highscore; @@ -155,13 +168,11 @@ double quiesce(struct pos const* pos, } size_t move_count = 0; - struct move moves[MOVE_MAX]; all_pseudolegal_moves(pos, MG_CAPTURES, us, &move_count, moves); if (move_count == 0) { - /* TODO: detect stalemate */ - return -(999.0 + (double)depth); + return -(SCORE_CHECKMATE + (float)depth); } for (size_t i = 0; i < move_count; ++i) { move_compute_appeal(&moves[i], pos, us, mailbox); @@ -180,7 +191,7 @@ double quiesce(struct pos const* pos, /* history is irrelevant when all moves are captures */ static struct history hist; hist.length = 0; - (void)move_piece(&poscpy, &hist, mailbox_cpy, m); + (void)move_piece(&poscpy, us, &hist, mailbox_cpy, m); score = -quiesce(&poscpy, mailbox_cpy, them, -beta, -alpha, depth - 1); @@ -208,15 +219,26 @@ struct search_option alphabeta_search(struct pos const* pos, Piece8 mailbox[restrict static SQ_COUNT], Side8 us, int8_t depth, - double alpha, - double beta, - atomic_bool* searching) + float alpha, + float beta + #ifdef FEATURE_STOPPABLE_SEARCH + , + struct searching_flag const* restrict searching + #endif + ) { g_ab_node_volume += 1; + +#ifdef FEATURE_STOPPABLE_SEARCH + if (!searching_still(searching)) { + return (struct search_option) { .init = false, .move = MOVE_NULL }; + } +#endif + if (pos->pieces[us][PIECE_KING] == 0) { return (struct search_option) { - .score = -(SCORE_CHECKMATE + (double)depth), + .score = -(SCORE_CHECKMATE + (float)depth), .move = (struct move){0}, .depth = 0, .hash = pos->hash, @@ -226,7 +248,7 @@ struct search_option alphabeta_search(struct pos const* pos, } if (depth <= 0) { - double sc = quiesce(pos, mailbox, us, alpha, beta, 0); + float sc = quiesce(pos, mailbox, us, alpha, beta, 0); return (struct search_option){ .score = sc, .move = (struct move){0}, @@ -237,15 +259,20 @@ struct search_option alphabeta_search(struct pos const* pos, }; } - double const alpha_orig = alpha; + float const alpha_orig = alpha; struct move moves[MOVE_MAX]; size_t move_count = 0; + float best_score = -SCORE_INF; + struct move best_move = MOVE_NULL; + bool has_principal_move = false; + struct search_option tte = tt_get(tt, pos->hash); if (tte.init && tte.hash == pos->hash) { if (tte.depth >= depth) { if (tte.flag == TT_EXACT) { + assuming(tte.init); return tte; } else if (tte.flag == TT_LOWER) { if (tte.score > alpha) alpha = tte.score; @@ -254,6 +281,7 @@ struct search_option alphabeta_search(struct pos const* pos, } if (alpha >= beta) { + assuming(tte.init); return tte; } } @@ -261,23 +289,23 @@ struct search_option alphabeta_search(struct pos const* pos, moves[move_count] = tte.move; moves[move_count].appeal = APPEAL_MAX; ++move_count; + has_principal_move = true; } - double best_score = -SCORE_INF; - struct move best_move = NULL_MOVE; - enum move_gen_type const types[] = {MG_CAPTURES, MG_CHECKS, MG_QUIETS}; for (size_t i = 0; i < sizeof types / sizeof *types; ++i) { all_pseudolegal_moves(pos, types[i], us, &move_count, moves); - for (size_t i = 0; i < move_count; ++i) { + for (size_t i = (size_t)has_principal_move; i < move_count; ++i) { move_compute_appeal(&moves[i], pos, us, mailbox); } while (move_count > 0) { - if (!atomic_load_explicit(searching, memory_order_relaxed)) { +#ifdef FEATURE_STOPPABLE_SEARCH + if (!searching_still(searching)) { return (struct search_option) { .init = false }; } +#endif struct move m = moves_linear_search(moves, &move_count); size_t const old_hist_len = hist->length; @@ -285,9 +313,9 @@ struct search_option alphabeta_search(struct pos const* pos, Piece8 mailbox_cpy[SQ_COUNT]; my_memcpy(mailbox_cpy, mailbox, sizeof mailbox_cpy); - enum move_result r = move_piece(&pos_cpy, hist, mailbox_cpy, m); + enum move_result r = move_piece(&pos_cpy, us, hist, mailbox_cpy, m); - double score; + float score = 0.0; if (r == MR_STALEMATE || r == MR_REPEATS) { score = 0.0; @@ -299,15 +327,18 @@ struct search_option alphabeta_search(struct pos const* pos, other_side(us), depth - 1, -beta, - -alpha, - searching); + -alpha + #ifdef FEATURE_STOPPABLE_SEARCH + , + searching + #endif + ); if (!so.init) { hist->length = old_hist_len; - return (struct search_option){ .init = false, .move = NULL_MOVE }; + return so; } score = -so.score; } - hist->length = old_hist_len; if (score > best_score) { @@ -325,8 +356,8 @@ struct search_option alphabeta_search(struct pos const* pos, finish_search: - if (IS_NULL_MOVE(best_move)) { - return (struct search_option){ .init = false, .move = NULL_MOVE }; + if (IS_MOVE_NULL(best_move)) { + return (struct search_option){ .init = true, .move = MOVE_NULL, .score = -(SCORE_CHECKMATE + (float)depth) }; } enum tt_flag flag = TT_EXACT; @@ -348,15 +379,31 @@ finish_search: tt_insert(tt, pos->hash, out); + assuming(out.init); return out; } static -struct search_result {struct move move; double score;} - search(struct board* b, Side8 us, int8_t max_depth, atomic_bool* searching) +struct search_result {struct move move; float score;} + search( + struct board* restrict b, + Side8 us, + int8_t max_depth + #ifdef FEATURE_STOPPABLE_SEARCH + , + struct searching_flag const* restrict searching + #endif + ) { struct move moves[MOVE_MAX]; size_t move_count = 0; + +#ifndef NDEBUG + for (size_t i = 0; i < MOVE_MAX; ++i) { + moves[i] = MOVE_POISONED; + } +#endif + all_pseudolegal_moves(&b->pos, MG_ALL, us, &move_count, moves); assuming(move_count); @@ -365,26 +412,36 @@ struct search_result {struct move move; double score;} g_ab_node_volume = 0; - double score = 0.0; + float score = 0.0; for (int8_t d = 1; - d <= max_depth && atomic_load_explicit(searching, memory_order_relaxed); + d <= max_depth +#ifdef FEATURE_STOPPABLE_SEARCH + && searching_still(searching) +#endif + ; ++d) { - double window = SCORE_INF; /* temp debug solution */ - double alpha = score - window; - double beta = score + window; + float window = 2*SCORE_INF; /* temp debug solution */ + float alpha = score - window; + float beta = score + window; while (1) { struct search_option so = - alphabeta_search(&b->pos, &b->hist, &b->tt, b->mailbox, us, d, alpha, beta, searching); + alphabeta_search(&b->pos, &b->hist, &b->tt, b->mailbox, us, d, alpha, beta +#ifdef FEATURE_STOPPABLE_SEARCH + , searching +#endif + ); - if (atomic_load_explicit(searching, memory_order_relaxed) == false) { - break; +#ifdef FEATURE_STOPPABLE_SEARCH + if (!searching_still(searching)) { + goto breakbreak; } +#endif - if (IS_NULL_MOVE(so.move)) { - break; + if (IS_MOVE_NULL(so.move)) { + goto breakbreak; } if (so.score > alpha && so.score < beta) { @@ -404,14 +461,15 @@ struct search_result {struct move move; double score;} } } -#ifdef USE_PRINTF +#ifdef FEATURE_USE_PRINTF fprintf(stderr, "depth: %hhd\n", d); #endif } +breakbreak: #undef SCORE_INF -#ifdef USE_PRINTF +#ifdef FEATURE_USE_PRINTF fprintf(stderr, "nodes searched: %'llu\n", g_ab_node_volume); #endif diff --git a/tests.c b/tests.c index 2af8b4f..7c3e506 100644 --- a/tests.c +++ b/tests.c @@ -1,5 +1,8 @@ -#define USE_PRINTF +#define FEATURE_STOPPABLE_SEARCH +#define FEATURE_USE_PRINTF +#undef NSTATS + #define _XOPEN_SOURCE 500 #include /* usleep */ #include @@ -390,15 +393,15 @@ static void test_bishops(void) } struct timeout_params { - atomic_bool* x; - bool v; + struct searching_flag* x; + uint32_t v; useconds_t us; }; void* set_after_timeout(void* x) { struct timeout_params* p = x; usleep(p->us); - *p->x = p->v; + searching_stop(p->x); return NULL; } @@ -406,9 +409,11 @@ int main() { bool const print_threats = true; - printf("sizeof pos: %zu\n", sizeof (struct pos)); - printf("sizeof tt: %zu\n", sizeof (struct tt)); - printf("sizeof board: %zu\n", sizeof (struct board)); + printf("sizeof pos: %zu\n", sizeof (struct pos)); + printf("sizeof tt: %zu\n", sizeof (struct tt)); + printf("sizeof board: %zu\n", sizeof (struct board)); + printf("sizeof search_option: %zu\n", sizeof (struct search_option)); + printf("sizeof all tt entries: %zu\n", (1<<26) * sizeof (struct search_option)); #if 0 test_rooks(); @@ -431,6 +436,7 @@ int main() //board_load_fen_unsafe(b, "1n1q1rk1/r1p2P2/1p1pp2p/pB2P3/2P5/PPN5/6b1/3QK1NR b - - 0 1"); //board_load_fen_unsafe(b, "8/8/2kr4/6R1/4K3/6P1/8/8 b - - 0 1"); //board_load_fen_unsafe(b, "8/8/5R2/8/2K3PP/1B2k3/8/8 b - - 1 4"); + //board_load_fen_unsafe(b, "4r1k1/b1P1B3/P5pK/1p4P1/7q/8/4p3/1R6 w - - 1 54"); //board_print_fen(b->pos, stdout); board_print(&b->pos, NULL, stdout, print_threats); @@ -438,6 +444,7 @@ int main() size_t move_count; for (int turn = 0; turn < 200; ++turn) { + /* move_count = 0; all_pseudolegal_moves(&b->pos, MG_ALL, b->pos.moving_side, &move_count, moves); @@ -448,10 +455,12 @@ int main() board_print(&b->pos, NULL, stdout, print_threats); break; } + */ pthread_t timer; - atomic_bool searching; - atomic_init(&searching, true); + struct searching_flag searching; + searching_start(&searching); + //atomic_init(&searching, 1); #if 1 struct timeout_params timer_params = { .x = &searching, @@ -482,11 +491,18 @@ int main() enum move_result const r = board_move(b, move); + /* illegal board state from an engine move (i.e. hanging king) means checkmate */ + if (!board_is_legal(b)) { + printf("checkmate!\n"); + break; + } + #if 1 board_print_fen(&b->pos, stdout); tt_print_stats(&b->tt, stdout); board_print(&b->pos, &move, stdout, print_threats); fprintf(stderr, "board hist len: %zu\n", b->hist.length); + fprintf(stderr, "\n------------------------\n\n\n"); #endif if (r == MR_STALEMATE) { @@ -496,14 +512,13 @@ int main() if (b->pos.pieces[SIDE_WHITE][PIECE_KING] == 0ULL) { printf("white king gone!!\n"); - exit(1); - } - if (b->pos.pieces[SIDE_BLACK][PIECE_KING] == 0ULL) { - printf("black king gone!!\n"); - exit(1); + exit(EXIT_FAILURE); } - //usleep(1000000); + if (b->pos.pieces[SIDE_BLACK][PIECE_KING] == 0ULL) { + printf("black king gone!!\n"); + exit(EXIT_FAILURE); + } } return EXIT_SUCCESS; diff --git a/wasm-compat.c b/wasm-compat.c index 08749ee..ca2e5a7 100644 --- a/wasm-compat.c +++ b/wasm-compat.c @@ -9,75 +9,75 @@ 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_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, - }; + 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); + struct search_result const sr = search(&g_board, g_board.pos.player, max_depth); + return move_serialize(sr.move); } int32_t wb_move(uint32_t move) { - struct move const m = move_deserialize(move); - enum move_result const mr = board_move_2(&g_board, m); + struct move const m = move_deserialize(move); + enum move_result const mr = board_move_2(&g_board, m); - /* TODO: this checkmate/stalemate check needs to be abstracted better */ - if (mr == MR_STALEMATE) { - return (int32_t)MR_STALEMATE; - } - struct move moves[MOVE_MAX]; - size_t move_count = 0ULL; - all_moves(&g_board.pos, opposite_player(g_board.pos.player), &move_count, moves); - if (move_count == 0ULL) { - return MR_CHECKMATE; - } + /* TODO: this checkmate/stalemate check needs to be abstracted better */ + if (mr == MR_STALEMATE) { + return (int32_t)MR_STALEMATE; + } + struct move moves[MOVE_MAX]; + size_t move_count = 0ULL; + all_moves(&g_board.pos, opposite_player(g_board.pos.player), &move_count, moves); + if (move_count == 0ULL) { + return MR_CHECKMATE; + } - return (int32_t)mr; + return (int32_t)mr; } void wb_init() { - g_board = BOARD_INIT; - g_board.tt.entries = g_tt_buf; + 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); + 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; + 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; }