Make engine dep-free/standalone and add Wasm target

This commit is contained in:
2025-12-19 04:01:32 +01:00
parent 509f58a379
commit 02b1ca4cfe
10 changed files with 1264 additions and 392 deletions

View File

@@ -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

532
base.h
View File

@@ -1,32 +1,16 @@
#pragma once
#include <ctype.h>
#include <math.h>
#include <locale.h>
#include "libc-lite.h"
#include "sys.h"
#include <stdint.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#define index string_index
#include <string.h>
#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<<TT_ADDRESS_BITS)
struct tt {
struct search_option* entries; /* must be initialized somewhere */
struct search_option* entries/*[TT_ENTRIES]*/; /* must be initialized somewhere */
#ifndef NDEBUG
/* stats */
uint64_t collisions;
uint64_t hits;
uint64_t probes;
uint64_t overwritten;
uint64_t insertions;
#endif
};
@@ -906,7 +901,7 @@ struct board {
/* used for repeated board state detection only */
struct history {
struct pos items[4096];
struct pos items[64];
size_t length;
} hist;
@@ -936,7 +931,7 @@ static void move_compute_appeal(struct move* m,
m->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, "<TODO>");
} 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");
}

244
board_print.h Normal file
View File

@@ -0,0 +1,244 @@
#include <ctype.h>
#include <locale.h>
#include <stdio.h>
#include <inttypes.h>
#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");
}

35
chess.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Wasm Loader</title>
<script type="module" src="./chess.js" defer></script>
<style>
#board {
display: grid;
grid-template-columns: repeat(8, 60px);
grid-template-rows: repeat(8, 60px);
width: 480px;
}
.square {
width: 60px;
height: 60px;
font-size: 57px;
}
.square.light {
background: #f0d9b5;
}
.square.dark {
background: #b58863;
}
</style>
</head>
<body>
<div id="board"></div>
</body>
</html>

232
chess.js Normal file
View File

@@ -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);
});

202
evaluations.h Normal file
View File

@@ -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

80
libc-lite.h Normal file
View File

@@ -0,0 +1,80 @@
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
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;
}

194
sys.h Normal file
View File

@@ -0,0 +1,194 @@
#pragma once
#if defined(WASM)
#define PLATFORM_STR "Wasm"
#include <stdint.h>
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 <sys/mman.h>
#define index string_index
#include <string.h>
#undef index
#include <errno.h>
#include <stdio.h>
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 <windows.h>
#include <stdio.h>
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

51
tests.c
View File

@@ -3,9 +3,11 @@
#include <unistd.h> /* usleep */
#include "base.h"
#include "board_print.h"
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
/* 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);
}

71
wasm-compat.c Normal file
View File

@@ -0,0 +1,71 @@
#include "base.h"
#include <stdint.h>
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;
}