├── .gitignore ├── LICENSE ├── README.rst ├── setup.py ├── sisyphus ├── C-Thread-Pool │ ├── thpool.c │ └── thpool.h ├── Makefile ├── __init__.py ├── attacks.c ├── attacks.h ├── bb.c ├── bb.h ├── board.c ├── board.h ├── eval.c ├── eval.h ├── gen.c ├── gen.h ├── move.c ├── move.h ├── search.c ├── search.h ├── table.c ├── table.h ├── types.h ├── utils.c ├── utils.h ├── zobrist.c └── zobrist.h ├── test.py └── tools └── uci.py /.gitignore: -------------------------------------------------------------------------------- 1 | notes 2 | __pycache__ 3 | *.o 4 | *.out 5 | *.so 6 | *.so.1 7 | venv/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Salmi Younes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | Sisyphus is a chess engine and library that combines the raw performance of **C** with the flexibility and ease of use of **Python**. 5 | It features a simple implementation of the `Universal Chess Interface (UCI) `_ protocol, enabling seamless integration with popular chess GUIs. 6 | In addition to UCI support, Sisyphus provides efficient move generation, move validation, and other essential chess functionalities. 7 | 8 | Features 9 | -------- 10 | 11 | - **Board Representation** 12 | 13 | - `Bitboards `_ 14 | - `Magic Bitboards `_ 15 | 16 | - **Search Algorithms** 17 | 18 | - `Negamax `_ 19 | - `Principal Variation Search (PVS) `_ 20 | - `Transposition Table `_ 21 | - `Late Move Reductions (LMR) `_ 22 | - `Null Move Pruning `_ 23 | - `Razoring `_ 24 | - `Delta Pruning `_ 25 | - `Quiescence Search `_ 26 | - `Internal Iterative Reductions `_ 27 | - `Reverse Futility Pruning `_ 28 | - `Aspiration Window `_ 29 | 30 | - **Move Ordering Techniques** 31 | 32 | - `History Heuristic `_ 33 | - `Killer Heuristic `_ 34 | - `MVV-LVA (Most Valuable Victim - Least Valuable Attacker) `_ 35 | - `Static Exchange Evaluation (SEE) `_ 36 | 37 | - **Evaluation** 38 | 39 | - `Piece-Square Tables (PSTs) `_ 40 | - `PeSTO Evaluation Framework `_ 41 | 42 | Installing 43 | ---------- 44 | 45 | Prerequisites 46 | ~~~~~~~~~~~~ 47 | 48 | - Python 3.11+ 49 | - Git 50 | - pip (Python package installer) 51 | - A C compiler (gcc or clang) 52 | 53 | Installation Steps 54 | ~~~~~~~~~~~~~~~~ 55 | 56 | 1. Clone the repository:: 57 | 58 | git clone https://github.com/salmiyounes/Sisyphus.git 59 | 60 | 2. Navigate to the project directory:: 61 | 62 | cd Sisyphus/ 63 | 64 | 3. Install the package:: 65 | 66 | pip install . 67 | 68 | Use Sisyphus as library 69 | ----------------------- 70 | * Move generation 71 | 72 | .. code:: python 73 | 74 | >>> import sisyphus 75 | 76 | >>> board = sisyphus.Board() 77 | >>> board.gen_legal_moves 78 | LegalMoveGenerator at 0x7e1c20527fb0 (a2a3, b2b3, c2c3, d2d3, e2e3, f2f3, g2g3, h2h3, a2a4, b2b4, c2c4, d2d4, e2e4, f2f4, g2g4, h2h4, b1a3, b1c3, g1f3, g1h3) 79 | >>> 80 | >>> board.gen_pseudo_legal_moves 81 | PseudoLegalMoveGenerator at 0x7e1c203695b0 (a2a3, b2b3, c2c3, d2d3, e2e3, f2f3, g2g3, h2h3, a2a4, b2b4, c2c4, d2d4, e2e4, f2f4, g2g4, h2h4, b1a3, b1c3, g1f3, g1h3) 82 | 83 | * Show a simple ASCII board. 84 | 85 | .. code:: python 86 | 87 | >>> board = sisyphus.Board() 88 | >>> print(board) 89 | 8 r n b q k b n r 90 | 7 p p p p p p p p 91 | 6 . . . . . . . . 92 | 5 . . . . . . . . 93 | 4 . . . . . . . . 94 | 3 . . . . . . . . 95 | 2 P P P P P P P P 96 | 1 R N B Q K B N R 97 | a b c d e f g h 98 | 99 | >>> board.unicode() # For pretty-printing 100 | 8 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 101 | 7 ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 102 | 6 . . . . . . . . 103 | 5 . . . . . . . . 104 | 4 . . . . . . . . 105 | 3 . . . . . . . . 106 | 2 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 107 | 1 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 108 | a b c d e f g h 109 | 110 | 111 | rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 112 | 113 | * Make and unmake moves 114 | 115 | .. code:: python 116 | 117 | >>> move = sisyphus.Move.parse_uci(board, 'e2e3') # Or sisyphus.Move(sisyphus.E2, sisyphus.E3, sisyphus.PieceType(sisyphus.PAWN, sisyphus.WHITE)) 118 | >>> move in board.gen_legal_moves 119 | True 120 | >>> board.push(move) # Make move 121 | 122 | >>> board.pop() # Unmake move 123 | Move(san=e2e3, from=12, to=20, piece=PieceType'P', flag=0) 124 | 125 | * Detects checkmates, stalemates and draws by insufficient material. 126 | 127 | .. code:: python 128 | 129 | >>> board = sisyphus.Board("r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4") 130 | >>> board.is_stalemate() 131 | False 132 | >>> board.drawn_by_insufficient_material() 133 | False 134 | >>> board.is_checkmate() 135 | True 136 | 137 | * Detects checks and attacks. 138 | 139 | .. code:: python 140 | 141 | >>> board.is_check() 142 | True 143 | >>> board.is_square_attacked_by(sisyphus.WHITE, sisyphus.E8) 144 | True 145 | 146 | >>> attackers = board.attackers(sisyphus.WHITE, sisyphus.F3) 147 | >>> attackers 148 | SquareSet(0x0000_0000_0000_4040) 149 | >>> sisyphus.G1 in attackers 150 | True 151 | >>> print(attackers) 152 | . . . . . . . . 153 | . . . . . . . . 154 | . . . . . . . . 155 | . . . . . . . . 156 | . . . . . . . . 157 | . . . . . . . . 158 | . . . . . . 1 . 159 | . . . . . . 1 . 160 | 161 | * Parses and creates FENs. 162 | 163 | .. code:: python 164 | 165 | >>> board.fen 166 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -' 167 | >>> 168 | >>> board = sisyphus.Board("rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3 0 1") 169 | >>> board.color_at(sisyphus.D1) # '0' for WHITE and '1' for BLACK 170 | 0 171 | 172 | * Performance test. 173 | 174 | .. code:: python 175 | 176 | >>> board.clear() 177 | >>> board.perft_test(6) 178 | 119060324 179 | 180 | * Running Tests 181 | 182 | .. code-block:: shell 183 | 184 | # Run all tests 185 | $ python3 tests.py 186 | 187 | # Run tests with verbose output 188 | $ python3 tests.py -v 189 | 190 | Play against Sisyphus! 191 | ---------------------- 192 | 193 | You can play against Sisyphus using any chess GUI that supports the UCI protocol, 194 | It has been primarily tested with `CuteChess `_, It also has a `Lichess account `_ where you can challenge it. 195 | 196 | License 197 | ------- 198 | 199 | Sisyphus is under the MIT License. 200 | Check out ``LICENSE.txt`` for the full text. 201 | 202 | Contributing 203 | ----------- 204 | 205 | Contributions are welcome! Please feel free to submit a Pull Request. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.command.build_py import build_py 3 | import subprocess 4 | 5 | class CustomBuildPyCommand(build_py): 6 | def run(self): 7 | subprocess.check_call( 8 | ['make', 'DEBUG=0', 'DEBUG_DISABLE_PRINT=1', 'DISABLE_ASSERT=1'], 9 | cwd='sisyphus' 10 | ) 11 | super().run() 12 | 13 | setup( 14 | name='sisyphus', 15 | version='0.1.0', 16 | author='Salmi Younes', 17 | description='Sisyphus: a Python Chess Engine/Library based on C backend', 18 | long_description=open('README.rst').read(), 19 | url='https://github.com/salmiyounes/Sisyphus', 20 | packages=['sisyphus'], 21 | package_dir={'sisyphus': 'sisyphus'}, 22 | package_data={'sisyphus': ['libchess.so']}, 23 | cmdclass={'build_py': CustomBuildPyCommand}, 24 | ) 25 | -------------------------------------------------------------------------------- /sisyphus/C-Thread-Pool/thpool.c: -------------------------------------------------------------------------------- 1 | /* ******************************** 2 | * Author: Johan Hanssen Seferidis 3 | * License: MIT 4 | * Description: Library providing a threading pool where you can add 5 | * work. For usage, check the thpool.h file or README.md 6 | * 7 | *//** @file thpool.h *//* 8 | * 9 | ********************************/ 10 | 11 | #if defined(__APPLE__) 12 | #include 13 | #else 14 | #ifndef _POSIX_C_SOURCE 15 | #define _POSIX_C_SOURCE 200809L 16 | #endif 17 | #ifndef _XOPEN_SOURCE 18 | #define _XOPEN_SOURCE 500 19 | #endif 20 | #endif 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #if defined(__linux__) 29 | #include 30 | #endif 31 | #if defined(__FreeBSD__) || defined(__OpenBSD__) 32 | #include 33 | #endif 34 | 35 | #include "thpool.h" 36 | 37 | #ifdef THPOOL_DEBUG 38 | #define THPOOL_DEBUG 1 39 | #else 40 | #define THPOOL_DEBUG 0 41 | #endif 42 | 43 | #if !defined(DISABLE_PRINT) || defined(THPOOL_DEBUG) 44 | #define err(str) fprintf(stderr, str) 45 | #else 46 | #define err(str) 47 | #endif 48 | 49 | #ifndef THPOOL_THREAD_NAME 50 | #define THPOOL_THREAD_NAME thpool 51 | #endif 52 | 53 | #define STRINGIFY(x) #x 54 | #define TOSTRING(x) STRINGIFY(x) 55 | 56 | static volatile int threads_keepalive; 57 | static volatile int threads_on_hold; 58 | 59 | 60 | 61 | /* ========================== STRUCTURES ============================ */ 62 | 63 | 64 | /* Binary semaphore */ 65 | typedef struct bsem { 66 | pthread_mutex_t mutex; 67 | pthread_cond_t cond; 68 | int v; 69 | } bsem; 70 | 71 | 72 | /* Job */ 73 | typedef struct job{ 74 | struct job* prev; /* pointer to previous job */ 75 | void (*function)(void* arg); /* function pointer */ 76 | void* arg; /* function's argument */ 77 | } job; 78 | 79 | 80 | /* Job queue */ 81 | typedef struct jobqueue{ 82 | pthread_mutex_t rwmutex; /* used for queue r/w access */ 83 | job *front; /* pointer to front of queue */ 84 | job *rear; /* pointer to rear of queue */ 85 | bsem *has_jobs; /* flag as binary semaphore */ 86 | int len; /* number of jobs in queue */ 87 | } jobqueue; 88 | 89 | 90 | /* Thread */ 91 | typedef struct thread{ 92 | int id; /* friendly id */ 93 | pthread_t pthread; /* pointer to actual thread */ 94 | struct thpool_* thpool_p; /* access to thpool */ 95 | } thread; 96 | 97 | 98 | /* Threadpool */ 99 | typedef struct thpool_{ 100 | thread** threads; /* pointer to threads */ 101 | volatile int num_threads_alive; /* threads currently alive */ 102 | volatile int num_threads_working; /* threads currently working */ 103 | pthread_mutex_t thcount_lock; /* used for thread count etc */ 104 | pthread_cond_t threads_all_idle; /* signal to thpool_wait */ 105 | jobqueue jobqueue; /* job queue */ 106 | } thpool_; 107 | 108 | 109 | 110 | 111 | 112 | /* ========================== PROTOTYPES ============================ */ 113 | 114 | 115 | static int thread_init(thpool_* thpool_p, struct thread** thread_p, int id); 116 | static void* thread_do(struct thread* thread_p); 117 | static void thread_hold(int sig_id); 118 | static void thread_destroy(struct thread* thread_p); 119 | 120 | static int jobqueue_init(jobqueue* jobqueue_p); 121 | static void jobqueue_clear(jobqueue* jobqueue_p); 122 | static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob_p); 123 | static struct job* jobqueue_pull(jobqueue* jobqueue_p); 124 | static void jobqueue_destroy(jobqueue* jobqueue_p); 125 | 126 | static void bsem_init(struct bsem *bsem_p, int value); 127 | static void bsem_reset(struct bsem *bsem_p); 128 | static void bsem_post(struct bsem *bsem_p); 129 | static void bsem_post_all(struct bsem *bsem_p); 130 | static void bsem_wait(struct bsem *bsem_p); 131 | 132 | 133 | 134 | 135 | 136 | /* ========================== THREADPOOL ============================ */ 137 | 138 | 139 | /* Initialise thread pool */ 140 | struct thpool_* thpool_init(int num_threads){ 141 | 142 | threads_on_hold = 0; 143 | threads_keepalive = 1; 144 | 145 | if (num_threads < 0){ 146 | num_threads = 0; 147 | } 148 | 149 | /* Make new thread pool */ 150 | thpool_* thpool_p; 151 | thpool_p = (struct thpool_*)malloc(sizeof(struct thpool_)); 152 | if (thpool_p == NULL){ 153 | err("thpool_init(): Could not allocate memory for thread pool\n"); 154 | return NULL; 155 | } 156 | thpool_p->num_threads_alive = 0; 157 | thpool_p->num_threads_working = 0; 158 | 159 | /* Initialise the job queue */ 160 | if (jobqueue_init(&thpool_p->jobqueue) == -1){ 161 | err("thpool_init(): Could not allocate memory for job queue\n"); 162 | free(thpool_p); 163 | return NULL; 164 | } 165 | 166 | /* Make threads in pool */ 167 | thpool_p->threads = (struct thread**)malloc(num_threads * sizeof(struct thread *)); 168 | if (thpool_p->threads == NULL){ 169 | err("thpool_init(): Could not allocate memory for threads\n"); 170 | jobqueue_destroy(&thpool_p->jobqueue); 171 | free(thpool_p); 172 | return NULL; 173 | } 174 | 175 | pthread_mutex_init(&(thpool_p->thcount_lock), NULL); 176 | pthread_cond_init(&thpool_p->threads_all_idle, NULL); 177 | 178 | /* Thread init */ 179 | int n; 180 | for (n=0; nthreads[n], n); 182 | #if THPOOL_DEBUG 183 | printf("THPOOL_DEBUG: Created thread %d in pool \n", n); 184 | #endif 185 | } 186 | 187 | /* Wait for threads to initialize */ 188 | while (thpool_p->num_threads_alive != num_threads) {} 189 | 190 | return thpool_p; 191 | } 192 | 193 | 194 | /* Add work to the thread pool */ 195 | int thpool_add_work(thpool_* thpool_p, void (*function_p)(void*), void* arg_p){ 196 | job* newjob; 197 | 198 | newjob=(struct job*)malloc(sizeof(struct job)); 199 | if (newjob==NULL){ 200 | err("thpool_add_work(): Could not allocate memory for new job\n"); 201 | return -1; 202 | } 203 | 204 | /* add function and argument */ 205 | newjob->function=function_p; 206 | newjob->arg=arg_p; 207 | 208 | /* add job to queue */ 209 | jobqueue_push(&thpool_p->jobqueue, newjob); 210 | 211 | return 0; 212 | } 213 | 214 | 215 | /* Wait until all jobs have finished */ 216 | void thpool_wait(thpool_* thpool_p){ 217 | pthread_mutex_lock(&thpool_p->thcount_lock); 218 | while (thpool_p->jobqueue.len || thpool_p->num_threads_working) { 219 | pthread_cond_wait(&thpool_p->threads_all_idle, &thpool_p->thcount_lock); 220 | } 221 | pthread_mutex_unlock(&thpool_p->thcount_lock); 222 | } 223 | 224 | 225 | /* Destroy the threadpool */ 226 | void thpool_destroy(thpool_* thpool_p){ 227 | /* No need to destroy if it's NULL */ 228 | if (thpool_p == NULL) return ; 229 | 230 | volatile int threads_total = thpool_p->num_threads_alive; 231 | 232 | /* End each thread 's infinite loop */ 233 | threads_keepalive = 0; 234 | 235 | /* Give one second to kill idle threads */ 236 | double TIMEOUT = 1.0; 237 | time_t start, end; 238 | double tpassed = 0.0; 239 | time (&start); 240 | while (tpassed < TIMEOUT && thpool_p->num_threads_alive){ 241 | bsem_post_all(thpool_p->jobqueue.has_jobs); 242 | time (&end); 243 | tpassed = difftime(end,start); 244 | } 245 | 246 | /* Poll remaining threads */ 247 | while (thpool_p->num_threads_alive){ 248 | bsem_post_all(thpool_p->jobqueue.has_jobs); 249 | sleep(1); 250 | } 251 | 252 | /* Job queue cleanup */ 253 | jobqueue_destroy(&thpool_p->jobqueue); 254 | /* Deallocs */ 255 | int n; 256 | for (n=0; n < threads_total; n++){ 257 | thread_destroy(thpool_p->threads[n]); 258 | } 259 | free(thpool_p->threads); 260 | free(thpool_p); 261 | } 262 | 263 | 264 | /* Pause all threads in threadpool */ 265 | void thpool_pause(thpool_* thpool_p) { 266 | int n; 267 | for (n=0; n < thpool_p->num_threads_alive; n++){ 268 | pthread_kill(thpool_p->threads[n]->pthread, SIGUSR1); 269 | } 270 | } 271 | 272 | 273 | /* Resume all threads in threadpool */ 274 | void thpool_resume(thpool_* thpool_p) { 275 | // resuming a single threadpool hasn't been 276 | // implemented yet, meanwhile this suppresses 277 | // the warnings 278 | (void)thpool_p; 279 | 280 | threads_on_hold = 0; 281 | } 282 | 283 | 284 | int thpool_num_threads_working(thpool_* thpool_p){ 285 | return thpool_p->num_threads_working; 286 | } 287 | 288 | 289 | 290 | 291 | 292 | /* ============================ THREAD ============================== */ 293 | 294 | 295 | /* Initialize a thread in the thread pool 296 | * 297 | * @param thread address to the pointer of the thread to be created 298 | * @param id id to be given to the thread 299 | * @return 0 on success, -1 otherwise. 300 | */ 301 | static int thread_init (thpool_* thpool_p, struct thread** thread_p, int id){ 302 | 303 | *thread_p = (struct thread*)malloc(sizeof(struct thread)); 304 | if (*thread_p == NULL){ 305 | err("thread_init(): Could not allocate memory for thread\n"); 306 | return -1; 307 | } 308 | 309 | (*thread_p)->thpool_p = thpool_p; 310 | (*thread_p)->id = id; 311 | 312 | pthread_create(&(*thread_p)->pthread, NULL, (void * (*)(void *)) thread_do, (*thread_p)); 313 | pthread_detach((*thread_p)->pthread); 314 | return 0; 315 | } 316 | 317 | 318 | /* Sets the calling thread on hold */ 319 | static void thread_hold(int sig_id) { 320 | (void)sig_id; 321 | threads_on_hold = 1; 322 | while (threads_on_hold){ 323 | sleep(1); 324 | } 325 | } 326 | 327 | 328 | /* What each thread is doing 329 | * 330 | * In principle this is an endless loop. The only time this loop gets interrupted is once 331 | * thpool_destroy() is invoked or the program exits. 332 | * 333 | * @param thread thread that will run this function 334 | * @return nothing 335 | */ 336 | static void* thread_do(struct thread* thread_p){ 337 | 338 | /* Set thread name for profiling and debugging */ 339 | char thread_name[16] = {0}; 340 | 341 | snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id); 342 | 343 | #if defined(__linux__) 344 | /* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */ 345 | prctl(PR_SET_NAME, thread_name); 346 | #elif defined(__APPLE__) && defined(__MACH__) 347 | pthread_setname_np(thread_name); 348 | #elif defined(__FreeBSD__) || defined(__OpenBSD__) 349 | pthread_set_name_np(thread_p->pthread, thread_name); 350 | #else 351 | err("thread_do(): pthread_setname_np is not supported on this system"); 352 | #endif 353 | 354 | /* Assure all threads have been created before starting serving */ 355 | thpool_* thpool_p = thread_p->thpool_p; 356 | 357 | /* Register signal handler */ 358 | struct sigaction act; 359 | sigemptyset(&act.sa_mask); 360 | act.sa_flags = SA_ONSTACK; 361 | act.sa_handler = thread_hold; 362 | if (sigaction(SIGUSR1, &act, NULL) == -1) { 363 | err("thread_do(): cannot handle SIGUSR1"); 364 | } 365 | 366 | /* Mark thread as alive (initialized) */ 367 | pthread_mutex_lock(&thpool_p->thcount_lock); 368 | thpool_p->num_threads_alive += 1; 369 | pthread_mutex_unlock(&thpool_p->thcount_lock); 370 | 371 | while(threads_keepalive){ 372 | 373 | bsem_wait(thpool_p->jobqueue.has_jobs); 374 | 375 | if (threads_keepalive){ 376 | 377 | pthread_mutex_lock(&thpool_p->thcount_lock); 378 | thpool_p->num_threads_working++; 379 | pthread_mutex_unlock(&thpool_p->thcount_lock); 380 | 381 | /* Read job from queue and execute it */ 382 | void (*func_buff)(void*); 383 | void* arg_buff; 384 | job* job_p = jobqueue_pull(&thpool_p->jobqueue); 385 | if (job_p) { 386 | func_buff = job_p->function; 387 | arg_buff = job_p->arg; 388 | func_buff(arg_buff); 389 | free(job_p); 390 | } 391 | 392 | pthread_mutex_lock(&thpool_p->thcount_lock); 393 | thpool_p->num_threads_working--; 394 | if (!thpool_p->num_threads_working) { 395 | pthread_cond_signal(&thpool_p->threads_all_idle); 396 | } 397 | pthread_mutex_unlock(&thpool_p->thcount_lock); 398 | 399 | } 400 | } 401 | pthread_mutex_lock(&thpool_p->thcount_lock); 402 | thpool_p->num_threads_alive --; 403 | pthread_mutex_unlock(&thpool_p->thcount_lock); 404 | 405 | return NULL; 406 | } 407 | 408 | 409 | /* Frees a thread */ 410 | static void thread_destroy (thread* thread_p){ 411 | free(thread_p); 412 | } 413 | 414 | 415 | 416 | 417 | 418 | /* ============================ JOB QUEUE =========================== */ 419 | 420 | 421 | /* Initialize queue */ 422 | static int jobqueue_init(jobqueue* jobqueue_p){ 423 | jobqueue_p->len = 0; 424 | jobqueue_p->front = NULL; 425 | jobqueue_p->rear = NULL; 426 | 427 | jobqueue_p->has_jobs = (struct bsem*)malloc(sizeof(struct bsem)); 428 | if (jobqueue_p->has_jobs == NULL){ 429 | return -1; 430 | } 431 | 432 | pthread_mutex_init(&(jobqueue_p->rwmutex), NULL); 433 | bsem_init(jobqueue_p->has_jobs, 0); 434 | 435 | return 0; 436 | } 437 | 438 | 439 | /* Clear the queue */ 440 | static void jobqueue_clear(jobqueue* jobqueue_p){ 441 | 442 | while(jobqueue_p->len){ 443 | free(jobqueue_pull(jobqueue_p)); 444 | } 445 | 446 | jobqueue_p->front = NULL; 447 | jobqueue_p->rear = NULL; 448 | bsem_reset(jobqueue_p->has_jobs); 449 | jobqueue_p->len = 0; 450 | 451 | } 452 | 453 | 454 | /* Add (allocated) job to queue 455 | */ 456 | static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob){ 457 | 458 | pthread_mutex_lock(&jobqueue_p->rwmutex); 459 | newjob->prev = NULL; 460 | 461 | switch(jobqueue_p->len){ 462 | 463 | case 0: /* if no jobs in queue */ 464 | jobqueue_p->front = newjob; 465 | jobqueue_p->rear = newjob; 466 | break; 467 | 468 | default: /* if jobs in queue */ 469 | jobqueue_p->rear->prev = newjob; 470 | jobqueue_p->rear = newjob; 471 | 472 | } 473 | jobqueue_p->len++; 474 | 475 | bsem_post(jobqueue_p->has_jobs); 476 | pthread_mutex_unlock(&jobqueue_p->rwmutex); 477 | } 478 | 479 | 480 | /* Get first job from queue(removes it from queue) 481 | * Notice: Caller MUST hold a mutex 482 | */ 483 | static struct job* jobqueue_pull(jobqueue* jobqueue_p){ 484 | 485 | pthread_mutex_lock(&jobqueue_p->rwmutex); 486 | job* job_p = jobqueue_p->front; 487 | 488 | switch(jobqueue_p->len){ 489 | 490 | case 0: /* if no jobs in queue */ 491 | break; 492 | 493 | case 1: /* if one job in queue */ 494 | jobqueue_p->front = NULL; 495 | jobqueue_p->rear = NULL; 496 | jobqueue_p->len = 0; 497 | break; 498 | 499 | default: /* if >1 jobs in queue */ 500 | jobqueue_p->front = job_p->prev; 501 | jobqueue_p->len--; 502 | /* more than one job in queue -> post it */ 503 | bsem_post(jobqueue_p->has_jobs); 504 | 505 | } 506 | 507 | pthread_mutex_unlock(&jobqueue_p->rwmutex); 508 | return job_p; 509 | } 510 | 511 | 512 | /* Free all queue resources back to the system */ 513 | static void jobqueue_destroy(jobqueue* jobqueue_p){ 514 | jobqueue_clear(jobqueue_p); 515 | free(jobqueue_p->has_jobs); 516 | } 517 | 518 | 519 | 520 | 521 | 522 | /* ======================== SYNCHRONISATION ========================= */ 523 | 524 | 525 | /* Init semaphore to 1 or 0 */ 526 | static void bsem_init(bsem *bsem_p, int value) { 527 | if (value < 0 || value > 1) { 528 | err("bsem_init(): Binary semaphore can take only values 1 or 0"); 529 | exit(1); 530 | } 531 | pthread_mutex_init(&(bsem_p->mutex), NULL); 532 | pthread_cond_init(&(bsem_p->cond), NULL); 533 | bsem_p->v = value; 534 | } 535 | 536 | 537 | /* Reset semaphore to 0 */ 538 | static void bsem_reset(bsem *bsem_p) { 539 | pthread_mutex_destroy(&(bsem_p->mutex)); 540 | pthread_cond_destroy(&(bsem_p->cond)); 541 | bsem_init(bsem_p, 0); 542 | } 543 | 544 | 545 | /* Post to at least one thread */ 546 | static void bsem_post(bsem *bsem_p) { 547 | pthread_mutex_lock(&bsem_p->mutex); 548 | bsem_p->v = 1; 549 | pthread_cond_signal(&bsem_p->cond); 550 | pthread_mutex_unlock(&bsem_p->mutex); 551 | } 552 | 553 | 554 | /* Post to all threads */ 555 | static void bsem_post_all(bsem *bsem_p) { 556 | pthread_mutex_lock(&bsem_p->mutex); 557 | bsem_p->v = 1; 558 | pthread_cond_broadcast(&bsem_p->cond); 559 | pthread_mutex_unlock(&bsem_p->mutex); 560 | } 561 | 562 | 563 | /* Wait on semaphore until semaphore has value 0 */ 564 | static void bsem_wait(bsem* bsem_p) { 565 | pthread_mutex_lock(&bsem_p->mutex); 566 | while (bsem_p->v != 1) { 567 | pthread_cond_wait(&bsem_p->cond, &bsem_p->mutex); 568 | } 569 | bsem_p->v = 0; 570 | pthread_mutex_unlock(&bsem_p->mutex); 571 | } -------------------------------------------------------------------------------- /sisyphus/C-Thread-Pool/thpool.h: -------------------------------------------------------------------------------- 1 | /********************************** 2 | * @author Johan Hanssen Seferidis 3 | * License: MIT 4 | * 5 | **********************************/ 6 | 7 | #ifndef _THPOOL_ 8 | #define _THPOOL_ 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | /* =================================== API ======================================= */ 15 | 16 | 17 | typedef struct thpool_* threadpool; 18 | 19 | 20 | /** 21 | * @brief Initialize threadpool 22 | * 23 | * Initializes a threadpool. This function will not return until all 24 | * threads have initialized successfully. 25 | * 26 | * @example 27 | * 28 | * .. 29 | * threadpool thpool; //First we declare a threadpool 30 | * thpool = thpool_init(4); //then we initialize it to 4 threads 31 | * .. 32 | * 33 | * @param num_threads number of threads to be created in the threadpool 34 | * @return threadpool created threadpool on success, 35 | * NULL on error 36 | */ 37 | threadpool thpool_init(int num_threads); 38 | 39 | 40 | /** 41 | * @brief Add work to the job queue 42 | * 43 | * Takes an action and its argument and adds it to the threadpool's job queue. 44 | * If you want to add to work a function with more than one arguments then 45 | * a way to implement this is by passing a pointer to a structure. 46 | * 47 | * NOTICE: You have to cast both the function and argument to not get warnings. 48 | * 49 | * @example 50 | * 51 | * void print_num(int num){ 52 | * printf("%d\n", num); 53 | * } 54 | * 55 | * int main() { 56 | * .. 57 | * int a = 10; 58 | * thpool_add_work(thpool, (void*)print_num, (void*)a); 59 | * .. 60 | * } 61 | * 62 | * @param threadpool threadpool to which the work will be added 63 | * @param function_p pointer to function to add as work 64 | * @param arg_p pointer to an argument 65 | * @return 0 on success, -1 otherwise. 66 | */ 67 | int thpool_add_work(threadpool, void (*function_p)(void*), void* arg_p); 68 | 69 | 70 | /** 71 | * @brief Wait for all queued jobs to finish 72 | * 73 | * Will wait for all jobs - both queued and currently running to finish. 74 | * Once the queue is empty and all work has completed, the calling thread 75 | * (probably the main program) will continue. 76 | * 77 | * Smart polling is used in wait. The polling is initially 0 - meaning that 78 | * there is virtually no polling at all. If after 1 seconds the threads 79 | * haven't finished, the polling interval starts growing exponentially 80 | * until it reaches max_secs seconds. Then it jumps down to a maximum polling 81 | * interval assuming that heavy processing is being used in the threadpool. 82 | * 83 | * @example 84 | * 85 | * .. 86 | * threadpool thpool = thpool_init(4); 87 | * .. 88 | * // Add a bunch of work 89 | * .. 90 | * thpool_wait(thpool); 91 | * puts("All added work has finished"); 92 | * .. 93 | * 94 | * @param threadpool the threadpool to wait for 95 | * @return nothing 96 | */ 97 | void thpool_wait(threadpool); 98 | 99 | 100 | /** 101 | * @brief Pauses all threads immediately 102 | * 103 | * The threads will be paused no matter if they are idle or working. 104 | * The threads return to their previous states once thpool_resume 105 | * is called. 106 | * 107 | * While the thread is being paused, new work can be added. 108 | * 109 | * @example 110 | * 111 | * threadpool thpool = thpool_init(4); 112 | * thpool_pause(thpool); 113 | * .. 114 | * // Add a bunch of work 115 | * .. 116 | * thpool_resume(thpool); // Let the threads start their magic 117 | * 118 | * @param threadpool the threadpool where the threads should be paused 119 | * @return nothing 120 | */ 121 | void thpool_pause(threadpool); 122 | 123 | 124 | /** 125 | * @brief Unpauses all threads if they are paused 126 | * 127 | * @example 128 | * .. 129 | * thpool_pause(thpool); 130 | * sleep(10); // Delay execution 10 seconds 131 | * thpool_resume(thpool); 132 | * .. 133 | * 134 | * @param threadpool the threadpool where the threads should be unpaused 135 | * @return nothing 136 | */ 137 | void thpool_resume(threadpool); 138 | 139 | 140 | /** 141 | * @brief Destroy the threadpool 142 | * 143 | * This will wait for the currently active threads to finish and then 'kill' 144 | * the whole threadpool to free up memory. 145 | * 146 | * @example 147 | * int main() { 148 | * threadpool thpool1 = thpool_init(2); 149 | * threadpool thpool2 = thpool_init(2); 150 | * .. 151 | * thpool_destroy(thpool1); 152 | * .. 153 | * return 0; 154 | * } 155 | * 156 | * @param threadpool the threadpool to destroy 157 | * @return nothing 158 | */ 159 | void thpool_destroy(threadpool); 160 | 161 | 162 | /** 163 | * @brief Show currently working threads 164 | * 165 | * Working threads are the threads that are performing work (not idle). 166 | * 167 | * @example 168 | * int main() { 169 | * threadpool thpool1 = thpool_init(2); 170 | * threadpool thpool2 = thpool_init(2); 171 | * .. 172 | * printf("Working threads: %d\n", thpool_num_threads_working(thpool1)); 173 | * .. 174 | * return 0; 175 | * } 176 | * 177 | * @param threadpool the threadpool of interest 178 | * @return integer number of threads working 179 | */ 180 | int thpool_num_threads_working(threadpool); 181 | 182 | 183 | #ifdef __cplusplus 184 | } 185 | #endif 186 | 187 | #endif -------------------------------------------------------------------------------- /sisyphus/Makefile: -------------------------------------------------------------------------------- 1 | CC = clang 2 | CFLAGS = -Wall -Wextra -Wshadow -std=c11 -fPIC -O3 3 | LINK_FLAGS = -pthread 4 | 5 | DEBUG ?= 1 6 | DISABLE_ASSERT ?= 0 7 | DEBUG_DISABLE_PRINT ?= 0 8 | 9 | ifeq ($(DEBUG),1) 10 | CFLAGS += -DDEBUG 11 | endif 12 | 13 | ifeq ($(DISABLE_ASSERT),1) 14 | CFLAGS += -DDISABLE_ASSERT 15 | endif 16 | 17 | ifeq ($(DEBUG_DISABLE_PRINT),1) 18 | CFLAGS += -DDEBUG_DISABLE_PRINT 19 | endif 20 | 21 | SRCS = utils.c zobrist.c bb.c attacks.c search.c board.c gen.c move.c table.c eval.c C-Thread-Pool/thpool.c 22 | HEADERS = types.h utils.h zobrist.h bb.h attacks.h search.h board.h gen.h move.h table.h eval.h C-Thread-Pool/thpool.h 23 | INCLUDES = -I. -I C-Thread-Pool 24 | 25 | OBJS = $(SRCS:.c=.o) 26 | DEPS = $(SRCS:.c=.d) 27 | 28 | TARGET = libchess.so.1 29 | 30 | all: $(TARGET) 31 | 32 | $(TARGET): $(OBJS) 33 | $(CC) -shared -Wl,-soname,libchess.so.1 -o $@ $(OBJS) $(LINK_FLAGS) 34 | ln -sf $@ libchess.so 35 | 36 | %.o: %.c $(HEADERS) 37 | $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ 38 | 39 | -include $(DEPS) 40 | 41 | clean: 42 | rm -f $(OBJS) $(TARGET) libchess.so 43 | 44 | clean-precompiled: 45 | rm -f *.gch 46 | 47 | clean-deps: 48 | rm -f $(DEPS) 49 | 50 | clean-all: clean clean-precompiled clean-deps 51 | 52 | .PHONY: all clean clean-precompiled clean-deps clean-all -------------------------------------------------------------------------------- /sisyphus/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Chess Engine Interface 3 | 4 | This module connects Python to a chess engine written in C using ctypes. 5 | It includes tools for working with chess boards, pieces, and moves. 6 | 7 | Main features: 8 | - Create and manage chess games 9 | - Generate legal and pseudo-legal moves 10 | - Handle special rules like castling and en passant 11 | - Convert positions to and from FEN 12 | - Search for the best move using the engine 13 | 14 | """ 15 | 16 | from __future__ import annotations 17 | 18 | __author__ = "Salmi Younes" 19 | 20 | __version__ = "0.1.0" 21 | 22 | __license__ = "MIT" 23 | 24 | import os 25 | import dataclasses 26 | from typing import ( 27 | Optional, 28 | Callable, 29 | Union, 30 | List, 31 | Tuple, 32 | TypeAlias, 33 | Iterator, 34 | Self, 35 | Any, 36 | ) 37 | from ctypes import ( 38 | CDLL, 39 | Structure, 40 | POINTER, 41 | pointer, 42 | byref, 43 | Array, 44 | c_uint64, 45 | c_uint32, 46 | c_char, 47 | c_char_p, 48 | c_int, 49 | c_bool, 50 | c_void_p, 51 | c_float, 52 | create_string_buffer, 53 | ) 54 | 55 | # Starting possition fen 56 | STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -" 57 | 58 | 59 | BitBoard: TypeAlias = int 60 | 61 | Square: TypeAlias = int 62 | SQUARE_NB: Square = 64 63 | A1: Square = 0 64 | B1: Square = 1 65 | C1: Square = 2 66 | D1: Square = 3 67 | E1: Square = 4 68 | F1: Square = 5 69 | G1: Square = 6 70 | H1: Square = 7 71 | A2: Square = 8 72 | B2: Square = 9 73 | C2: Square = 10 74 | D2: Square = 11 75 | E2: Square = 12 76 | F2: Square = 13 77 | G2: Square = 14 78 | H2: Square = 15 79 | A3: Square = 16 80 | B3: Square = 17 81 | C3: Square = 18 82 | D3: Square = 19 83 | E3: Square = 20 84 | F3: Square = 21 85 | G3: Square = 22 86 | H3: Square = 23 87 | A4: Square = 24 88 | B4: Square = 25 89 | C4: Square = 26 90 | D4: Square = 27 91 | E4: Square = 28 92 | F4: Square = 29 93 | G4: Square = 30 94 | H4: Square = 31 95 | A5: Square = 32 96 | B5: Square = 33 97 | C5: Square = 34 98 | D5: Square = 35 99 | E5: Square = 36 100 | F5: Square = 37 101 | G5: Square = 38 102 | H5: Square = 39 103 | A6: Square = 40 104 | B6: Square = 41 105 | C6: Square = 42 106 | D6: Square = 43 107 | E6: Square = 44 108 | F6: Square = 45 109 | G6: Square = 46 110 | H6: Square = 47 111 | A7: Square = 48 112 | B7: Square = 49 113 | C7: Square = 50 114 | D7: Square = 51 115 | E7: Square = 52 116 | F7: Square = 53 117 | G7: Square = 54 118 | H7: Square = 55 119 | A8: Square = 56 120 | B8: Square = 57 121 | C8: Square = 58 122 | D8: Square = 59 123 | E8: Square = 60 124 | F8: Square = 61 125 | G8: Square = 62 126 | H8: Square = 63 127 | 128 | # Max moves 129 | MAX_MOVES: int = 218 130 | 131 | # Color number 132 | COLOR_NB: int = 2 133 | 134 | # Colors 135 | Color: TypeAlias = int 136 | WHITE: Color = 0 137 | BLACK: Color = 1 138 | BOTH: Color = 2 139 | 140 | # Flags 141 | Flags: TypeAlias = int 142 | EMPTY_FLAG: Flags = 0 143 | ENP_FLAG: Flags = 6 144 | CASTLE_FLAG: Flags = 1 145 | PROMO_FLAG: Flags = 8 146 | KNIGHT_PROMO_FLAG: Flags = 8 147 | ROOK_PROMO_FLAG: Flags = 9 148 | BISHOP_PROMO_FLAG: Flags = 10 149 | QUEEN_PROMO_FLAG: Flags = 11 150 | 151 | # Castling rights 152 | CASTLE_ALL: Flags = 15 153 | CASTLE_WHITE: Flags = 3 154 | CASTLE_BLACK: Flags = 12 155 | CASTLE_WHITE_KING_SIDE: Flags = 1 156 | CASTLE_WHITE_QUEEN_SIDE: Flags = 2 157 | CASTLE_BLACK_KING_SIDE: Flags = 4 158 | CASTLE_BLACK_QUEEN_SIDE: Flags = 8 159 | 160 | # piece 161 | Piece: TypeAlias = int 162 | PAWN: Piece = 0 163 | KNIGHT: Piece = 1 164 | BISHOP: Piece = 2 165 | ROOK: Piece = 3 166 | QUEEN: Piece = 4 167 | KING: Piece = 5 168 | NONE: Piece = 12 169 | 170 | # Piece symbols list 171 | PIECE_SYMBOLS: List[str] = ["p", "n", "b", "r", "q", "k"] 172 | 173 | 174 | class ChessBoard(Structure): 175 | """A C structure that represents a chess position using bitboards for piece placement, game state, and move history.""" 176 | _fields_ = [ 177 | ("squares", c_int * SQUARE_NB), 178 | ("numMoves", c_int), 179 | ("color", c_int), 180 | ("castle", c_int), 181 | ("mg", c_int * 2), 182 | ("eg", c_int * 2), 183 | ("gamePhase", c_int), 184 | ("bb_squares", c_uint64 * 12), 185 | ("m_history", c_uint64 * 8192), 186 | ("occ", c_uint64 * 3), 187 | ("ep", c_uint64), 188 | ("hash", c_uint64), 189 | ("pawn_hash", c_uint64), 190 | ] 191 | 192 | 193 | class Entry(Structure): 194 | """A C structure that represents a transposition table entry storing position evaluations and best moves.""" 195 | _fields_ = [ 196 | ("key", c_uint64), 197 | ("score", c_int), 198 | ("depth", c_int), 199 | ("flag", c_int), 200 | ] 201 | 202 | 203 | class Table(Structure): 204 | """A C structure representing a transposition table with size, mask and array of position entries.""" 205 | _fields_ = [("size", c_int), ("mask", c_int), ("entry", POINTER(Entry))] 206 | 207 | 208 | class Search(Structure): 209 | """A C structure that manages chess engine search state including node count, stop flag, and transposition table.""" 210 | _fields_ = [("nodes", c_int), ("stop", c_bool), ("num", c_int), ("table", Table)] 211 | 212 | 213 | class Undo(Structure): 214 | """A C structure that stores information needed to undo a chess move, including captured pieces, castling rights, and en passant state.""" 215 | _fields_ = [("capture", c_int), ("castle", c_int), ("ep", c_uint64)] 216 | 217 | 218 | try: 219 | chess_lib = CDLL(os.path.join(os.path.dirname(__file__), "libchess.so")) 220 | except OSError as e: 221 | raise RuntimeError(f"Could not load chess engine library: {e}") 222 | 223 | 224 | # Bitboard manipulation functions 225 | chess_lib.get_lsb.argtypes = [c_uint64] 226 | chess_lib.get_lsb.restype = c_int 227 | chess_lib.get_msb.argtypes = [c_uint64] 228 | chess_lib.get_msb.restype = c_int 229 | chess_lib.popcount.argtypes = [c_uint64] 230 | chess_lib.popcount.restype = c_int 231 | chess_lib.several.argtypes = [c_uint64] 232 | chess_lib.several.restype = c_int 233 | chess_lib.test_bit.argtypes = [c_uint64, c_int] 234 | chess_lib.test_bit.restype = c_bool 235 | chess_lib.square.argtypes = [c_int, c_int] 236 | chess_lib.square.restype = c_int 237 | chess_lib.file_of.argtypes = [c_int] 238 | chess_lib.file_of.restype = c_int 239 | chess_lib.rank_of.argtypes = [c_int] 240 | chess_lib.rank_of.restype = c_int 241 | chess_lib.bb_init.argtypes = [] 242 | chess_lib.bb_init.restype = c_void_p 243 | chess_lib.bb_print.argtypes = [c_uint64] 244 | chess_lib.bb_print.restype = c_void_p 245 | chess_lib.attacks_to_king_square.argtypes = [POINTER(ChessBoard), c_uint64] 246 | chess_lib.attacks_to_king_square.restype = c_int 247 | chess_lib.attacks_to_square.argtypes = [POINTER(ChessBoard), c_int, c_uint64] 248 | chess_lib.attacks_to_square.restype = c_uint64 249 | 250 | # Main board functions 251 | chess_lib.board_init.argtypes = [POINTER(ChessBoard)] 252 | chess_lib.board_init.restype = c_void_p 253 | chess_lib.board_load_fen.argtypes = [POINTER(ChessBoard), POINTER(c_char)] 254 | chess_lib.board_load_fen.restype = c_void_p 255 | chess_lib.print_board.argtypes = [POINTER(ChessBoard)] 256 | chess_lib.print_board.restype = c_void_p 257 | chess_lib.board_to_fen.argtypes = [POINTER(ChessBoard), POINTER(c_char)] 258 | chess_lib.board_to_fen.restype = c_void_p 259 | chess_lib.perft_test.argtypes = [POINTER(ChessBoard), c_int] 260 | chess_lib.perft_test.restype = c_uint64 261 | chess_lib.board_clear.argtypes = [POINTER(ChessBoard)] 262 | chess_lib.board_clear.restype = c_void_p 263 | chess_lib.board_drawn_by_insufficient_material.argtypes = [POINTER(ChessBoard)] 264 | chess_lib.board_drawn_by_insufficient_material.restype = c_int 265 | 266 | # Move Generation functions 267 | chess_lib.gen_black_attacks_against.argtypes = [ 268 | POINTER(ChessBoard), 269 | POINTER(c_uint32), 270 | c_uint64, 271 | ] 272 | chess_lib.gen_black_attacks_against.restype = c_int 273 | chess_lib.gen_white_attacks_against.argtypes = [ 274 | POINTER(ChessBoard), 275 | POINTER(c_uint32), 276 | c_uint64, 277 | ] 278 | chess_lib.gen_white_attacks_against.restype = c_int 279 | chess_lib.gen_attacks.argtypes = [POINTER(ChessBoard), POINTER(c_uint32)] 280 | chess_lib.gen_attacks.restype = c_int 281 | chess_lib.gen_moves.argtypes = [POINTER(ChessBoard), POINTER(c_uint32)] 282 | chess_lib.gen_moves.restype = c_int 283 | chess_lib.gen_legal_moves.argtypes = [POINTER(ChessBoard), POINTER(c_uint32)] 284 | chess_lib.gen_legal_moves.restype = c_int 285 | chess_lib.illegal_to_move.argtypes = [POINTER(ChessBoard)] 286 | chess_lib.illegal_to_move.restype = c_int 287 | chess_lib.is_check.argtypes = [POINTER(ChessBoard)] 288 | chess_lib.is_check.restype = c_int 289 | 290 | # Move Handling functions 291 | chess_lib.do_move.argtypes = [POINTER(ChessBoard), c_uint32, POINTER(Undo)] 292 | chess_lib.do_move.restype = c_void_p 293 | chess_lib.undo_move.argtypes = [POINTER(ChessBoard), c_uint32, POINTER(Undo)] 294 | chess_lib.undo_move.restype = c_void_p 295 | chess_lib.move_to_str.argtypes = [c_uint32] 296 | chess_lib.move_to_str.restype = c_char_p 297 | chess_lib.make_move.argtypes = [POINTER(ChessBoard), c_uint32] 298 | chess_lib.make_move.restype = c_void_p 299 | 300 | # Search and functions 301 | chess_lib.thread_init.argtypes = [ 302 | POINTER(Search), 303 | POINTER(ChessBoard), 304 | POINTER(c_uint32), 305 | c_float, 306 | c_bool, 307 | ] 308 | chess_lib.thread_init.restype = c_void_p 309 | chess_lib.thread_stop.argtypes = [POINTER(Search)] 310 | chess_lib.thread_stop.restype = c_void_p 311 | chess_lib.best_move.argtypes = [POINTER(Search), POINTER(ChessBoard), POINTER(c_uint32)] 312 | chess_lib.best_move.restype = c_int 313 | 314 | 315 | class IllegalMoveError(ValueError): 316 | """Exception raised when attempting to make an illegal chess move.""" 317 | pass 318 | 319 | 320 | class utils: 321 | """A collection of static utility methods for chess operations. 322 | 323 | This class provides helper methods for bit manipulation, square coordinates, 324 | move encoding and other chess-related calculations. 325 | """ 326 | 327 | @staticmethod 328 | def create_string_buffer(size: int) -> Array[Any]: 329 | """Create an array of unsigned 32-bit integers. 330 | 331 | Args: 332 | size: The size of the array to create 333 | 334 | Returns: 335 | A new uint32 array 336 | """ 337 | return create_string_buffer(size) 338 | 339 | @staticmethod 340 | def create_uint32_array(size: int) -> Array[Any]: 341 | """Create an array of unsigned 32-bit integers. 342 | 343 | Args: 344 | size: The size of the array to create 345 | 346 | Returns: 347 | A new uint32 array 348 | """ 349 | return (c_uint32 * size)() 350 | 351 | @staticmethod 352 | def get_lsb(bbit: BitBoard) -> int: 353 | """Get the least significant set bit in a bitboard. 354 | 355 | Args: 356 | bbit: The bitboard to examine 357 | 358 | Returns: 359 | The index of the least significant set bit 360 | """ 361 | lsb = chess_lib.get_lsb(bbit) 362 | return int(lsb) 363 | 364 | @staticmethod 365 | def get_msb(bbit: BitBoard) -> int: 366 | """Get the most significant set bit in a bitboard. 367 | 368 | Args: 369 | bbit: The bitboard to examine 370 | 371 | Returns: 372 | The index of the most significant set bit 373 | """ 374 | msb = chess_lib.get_msb(bbit) 375 | return int(msb) 376 | 377 | @staticmethod 378 | def popcount(bbit: BitBoard) -> int: 379 | """Count the number of set bits in a bitboard. 380 | 381 | Args: 382 | bbit: The bitboard to count bits in 383 | 384 | Returns: 385 | The number of set bits 386 | """ 387 | count = chess_lib.popcount(bbit) 388 | return int(count) 389 | 390 | @staticmethod 391 | def test_bit(bbit: BitBoard, sq: Square) -> bool: 392 | """Test if a specific bit is set in a bitboard. 393 | 394 | Args: 395 | bbit: The bitboard to test 396 | sq: The square index to check 397 | 398 | Returns: 399 | True if the bit is set, False otherwise 400 | """ 401 | bit = chess_lib.test_bit(bbit, sq) 402 | return bool(bit) 403 | 404 | @staticmethod 405 | def square(rank: Square, file: Square) -> Square: 406 | """Convert rank and file coordinates to a square index. 407 | 408 | Args: 409 | rank: The rank coordinate (0-7) 410 | file: The file coordinate (0-7) 411 | 412 | Returns: 413 | The square index (0-63) 414 | """ 415 | sq = chess_lib.square(rank, file) 416 | return Square(sq) 417 | 418 | @staticmethod 419 | def file_of(sq: Square) -> int: 420 | """Get the file of a square. 421 | 422 | Args: 423 | sq: The square index 424 | 425 | Returns: 426 | The file number (0-7) 427 | """ 428 | file = chess_lib.file_of(sq) 429 | return int(file) 430 | 431 | @staticmethod 432 | def rank_of(sq: Square) -> int: 433 | """Get the rank of a square. 434 | 435 | Args: 436 | sq: The square index 437 | 438 | Returns: 439 | The rank number (0-7) 440 | """ 441 | rank = chess_lib.rank_of(sq) 442 | return int(rank) 443 | 444 | @staticmethod 445 | def bit(sq: Square) -> int: 446 | """Convert a square index to a bitboard with only that bit set. 447 | 448 | Args: 449 | sq: The square index 450 | 451 | Returns: 452 | A bitboard with only the specified bit set 453 | """ 454 | return 1 << sq 455 | 456 | @staticmethod 457 | def scan_forward(bb: BitBoard) -> Iterator[Square]: 458 | """Iterate through set bits in a bitboard from LSB to MSB. 459 | 460 | Args: 461 | bb: The bitboard to scan 462 | 463 | Yields: 464 | Square indices of set bits 465 | """ 466 | while bb: 467 | r = bb & -bb 468 | yield r.bit_length() - 1 469 | bb ^= r 470 | 471 | @staticmethod 472 | def square_distance(a: Square, b: Square) -> int: 473 | """Calculate the Chebyshev distance between two squares. 474 | 475 | Args: 476 | a: First square index 477 | b: Second square index 478 | 479 | Returns: 480 | The number of king moves needed to go from a to b 481 | """ 482 | return max( 483 | abs(utils.file_of(a) - utils.file_of(b)), 484 | abs(utils.rank_of(a) - utils.rank_of(b)), 485 | ) 486 | 487 | @staticmethod 488 | def square_manhattan_distance(a: Square, b: Square) -> int: 489 | """Calculate the Manhattan distance between two squares. 490 | 491 | Args: 492 | a: First square index 493 | b: Second square index 494 | 495 | Returns: 496 | The sum of horizontal and vertical distances 497 | """ 498 | return abs(utils.file_of(a) - utils.file_of(b)) + abs( 499 | utils.rank_of(a) - utils.rank_of(b) 500 | ) 501 | 502 | @staticmethod 503 | def square_mirror(square: Square) -> Square: 504 | """Mirror a square vertically on the board. 505 | 506 | Args: 507 | square: The square index to mirror 508 | 509 | Returns: 510 | The mirrored square index 511 | """ 512 | return square ^ 56 513 | 514 | @staticmethod 515 | def encode_move(from_sq: int, to_sq: int, piece: int, flag: int) -> int: 516 | """Encode move information into a single integer. 517 | 518 | Args: 519 | from_sq: Source square (0-63) 520 | to_sq: Target square (0-63) 521 | piece: Piece type and color 522 | flag: Move flags (castling, en passant, promotion) 523 | 524 | Returns: 525 | Encoded move as a 32-bit integer 526 | """ 527 | return ( 528 | (from_sq & 0x3F) 529 | | ((to_sq & 0x3F) << 6) 530 | | ((piece & 0xF) << 12) 531 | | ((flag & 0xF) << 16) 532 | ) 533 | 534 | @staticmethod 535 | def scan_move_list(arr: List[int]) -> Iterator[List[Any]]: 536 | """Convert a list of encoded moves into move components. 537 | 538 | Args: 539 | arr: List of encoded move integers 540 | 541 | Yields: 542 | Lists containing [source square, target square, piece type, flags] 543 | """ 544 | 545 | # Helper Functions to extract specific fields 546 | def extract_source_sq(x: int) -> Square: 547 | return (x >> 0) & 0x3F 548 | 549 | def extract_target_sq(x: int) -> Square: 550 | return (x >> 6) & 0x3F 551 | 552 | def extract_piece_type(x: int) -> Optional[PieceType]: 553 | index: int = (x >> 12) & 0xF 554 | return PieceType.from_index(index) 555 | 556 | def extract_flag_type(x: int) -> Flags: 557 | return (x >> 16) & 0xF 558 | 559 | extractors: List[Callable[[int], Union[Square, Optional[PieceType], Flags]]] = [ 560 | extract_source_sq, 561 | extract_target_sq, 562 | extract_piece_type, 563 | extract_flag_type, 564 | ] 565 | 566 | for m in arr: 567 | yield [extr(m) for extr in extractors] 568 | 569 | 570 | @dataclasses.dataclass 571 | class PieceType: 572 | """Represents a chess piece with its type and color. 573 | 574 | This class encapsulates both the piece type (pawn, knight, bishop, etc.) 575 | and its color (white or black). It provides methods for converting between 576 | different piece representations and generating piece symbols. 577 | 578 | Attributes: 579 | piece (Piece): The type of piece (PAWN, KNIGHT, BISHOP, ROOK, QUEEN, or KING) 580 | color (Color): The piece color (WHITE or BLACK) 581 | 582 | Example: 583 | >>> piece = PieceType(KNIGHT, WHITE) 584 | PieceType'n' 585 | >>> print(piece.symbol()) 586 | 'n' 587 | >>> print(PieceType.from_symbol('p')) 588 | PieceType'p' 589 | >>> 590 | """ 591 | 592 | piece: Piece 593 | """The type of piece (0=PAWN through 5=KING)""" 594 | 595 | color: Color 596 | """The piece color (0=WHITE, 1=BLACK)""" 597 | 598 | def symbol(self) -> str: 599 | """Get the piece's symbol in algebraic notation. 600 | 601 | Returns: 602 | str: Uppercase letter for white pieces, lowercase for black pieces. 603 | P=pawn, N=knight, B=bishop, R=rook, Q=queen, K=king 604 | """ 605 | return ( 606 | PIECE_SYMBOLS[self.piece].upper() 607 | if not self.color 608 | else PIECE_SYMBOLS[self.piece] 609 | ) 610 | 611 | def __repr__(self) -> str: 612 | return f"{type(self).__name__}{self.symbol()!r}" 613 | 614 | def __hash__(self) -> int: 615 | """Generate a unique hash value for the piece. 616 | 617 | Combines piece type and color into a single integer for use as dictionary key. 618 | 619 | Returns: 620 | int: Unique hash value for this piece type and color combination 621 | """ 622 | return (self.piece * BOTH) + self.color 623 | 624 | @classmethod 625 | def from_index(cls, index: int) -> Optional[PieceType]: 626 | """Create a PieceType from an integer index representation. 627 | 628 | Args: 629 | index (int): Integer encoding both piece type and color 630 | 631 | Returns: 632 | Optional[PieceType]: New PieceType instance, or None if index represents NONE 633 | """ 634 | if index != NONE: # Check if 'index' is not a NONE piece type 635 | return cls( 636 | (index & ~BLACK) >> BLACK, # Get piece type 637 | index & BLACK, # Get color type 638 | ) 639 | else: 640 | return None 641 | 642 | @classmethod 643 | def from_symbol(cls, symbol: str) -> PieceType: 644 | """Create a PieceType from a piece symbol. 645 | 646 | Args: 647 | symbol (str): Single character piece symbol (e.g. 'P', 'n', 'K') 648 | Uppercase for white pieces, lowercase for black 649 | 650 | Returns: 651 | PieceType: New PieceType instance representing the piece 652 | 653 | Example: 654 | >>> PieceType.from_symbol('Q') # Black queen 655 | PieceType(piece=4, color=1) 656 | >>> PieceType.from_symbol('p') # White pawn 657 | PieceType(piece=0, color=0) 658 | """ 659 | return cls(PIECE_SYMBOLS.index(symbol.lower()), not symbol.islower()) 660 | 661 | 662 | @dataclasses.dataclass(slots=True) 663 | class Move: 664 | """Represents a chess move from one square to another. 665 | 666 | This class encapsulates all information about a chess move including the source and 667 | destination squares, the piece being moved, and special move flags for castling, 668 | en passant, and promotions. 669 | 670 | Attributes: 671 | from_sq (Square): The source square index (0-63) 672 | dst_sq (Square): The destination square index (0-63) 673 | piece (Optional[PieceType]): The piece being moved, or None 674 | flag (Flags): Special move flags: 675 | - EMPTY_FLAG (0): Normal move 676 | - CASTLE_FLAG (1): Castling move 677 | - ENP_FLAG (6): En passant capture 678 | - PROMO_FLAG (8): Pawn promotion base flag 679 | - KNIGHT_PROMO_FLAG (8): Promote to knight 680 | - BISHOP_PROMO_FLAG (10): Promote to bishop 681 | - ROOK_PROMO_FLAG (9): Promote to rook 682 | - QUEEN_PROMO_FLAG (11): Promote to queen 683 | 684 | Examples: 685 | >>> # Create a normal pawn move 686 | >>> move = Move(E2, E4, PieceType(PAWN, WHITE)) 687 | Move(san=e2e4, from=12, to=28, piece=PieceType'p', flag=0) 688 | >>> print(move.san) 689 | 'e2e4' 690 | 691 | >>> # Create a promotion move 692 | >>> move = Move(H7, H8, PieceType(PAWN, WHITE), PROMO_FLAG | QUEEN_PROMO_FLAG) 693 | >>> print(move.san) 694 | 'h7h8q' 695 | """ 696 | 697 | from_sq: Square 698 | """Source square""" 699 | 700 | dst_sq: Square 701 | """Destenation square""" 702 | 703 | piece: Optional[PieceType] = None 704 | """Piece type""" 705 | 706 | flag: Flags = EMPTY_FLAG 707 | """Flag type""" 708 | 709 | def __repr__(self) -> str: 710 | return f"{type(self).__name__}(san={self.san}, from={self.from_sq}, to={self.dst_sq}, piece={self.piece}, flag={self.flag})" 711 | 712 | def __str__(self) -> str: 713 | return self.san 714 | 715 | def __eq__(self, other: object) -> bool: 716 | if not isinstance(other, Move): 717 | return NotImplemented 718 | return ( 719 | self.dst_sq == other.dst_sq 720 | and self.from_sq == other.from_sq 721 | and self.piece == other.piece 722 | and self.flag == other.flag 723 | ) 724 | 725 | def __hash__(self) -> int: 726 | """Generate a unique hash value for the move. 727 | 728 | Encodes the move information into a 32-bit integer using the format: 729 | - bits 0-5: source square (0-63) 730 | - bits 6-11: destination square (0-63) 731 | - bits 12-15: piece type and color 732 | - bits 16-19: move flags 733 | 734 | Returns: 735 | int: Unique hash value for this move 736 | """ 737 | return utils.encode_move( 738 | self.from_sq, self.dst_sq, hash(self.piece) if self.piece else 0, self.flag 739 | ) 740 | 741 | def move_str(self) -> str: 742 | """Get the move in standard algebraic notation (SAN). 743 | 744 | Returns: 745 | str: The move in SAN format 746 | """ 747 | try: 748 | result = chess_lib.move_to_str(hash(self)) 749 | if not result: 750 | return "" 751 | return str(result.decode("utf-8")) 752 | except Exception as e: 753 | print(f"Error converting move to string: {e}") 754 | return "" 755 | 756 | @classmethod 757 | def parse_uci(cls, board: Board, uci: str) -> Move: 758 | if not isinstance(uci, str): 759 | raise ValueError("UCI must be a string") 760 | if len(uci) not in (4, 5): 761 | raise ValueError(f"Expected UCI string of length 4 or 5: {uci!r}") 762 | 763 | # file+rank -> 0..63 764 | def square(s: str) -> Square: 765 | f = ord(s[0]) - ord("a") 766 | r = int(s[1]) - 1 767 | if not (0 <= f <= 7 and 0 <= r <= 7): 768 | raise ValueError(f"Invalid square in UCI: {s!r}") 769 | return r * 8 + f 770 | 771 | from_sq = square(uci[0:2]) 772 | to_sq = square(uci[2:4]) 773 | 774 | # make sure there's something to move 775 | piece = board.board.piece_at(from_sq) 776 | if piece is None: 777 | raise ValueError(f"No piece on source square {uci[0:2]!r}") 778 | 779 | flag = EMPTY_FLAG 780 | 781 | # promotion? 782 | if len(uci) == 5: 783 | promo = uci[4].lower() 784 | if promo == "n": 785 | flag = KNIGHT_PROMO_FLAG 786 | elif promo == "b": 787 | flag = BISHOP_PROMO_FLAG 788 | elif promo == "r": 789 | flag = ROOK_PROMO_FLAG 790 | elif promo == "q": 791 | flag = QUEEN_PROMO_FLAG 792 | else: 793 | raise ValueError(f"Invalid promotion piece: {promo!r}") 794 | 795 | # castling? (king moves two files) 796 | if piece.piece == KING and abs((to_sq % 8) - (from_sq % 8)) == 2: 797 | flag = CASTLE_FLAG 798 | 799 | # en-passant? (pawn captures diagonally onto empty square) 800 | file_diff = abs((to_sq % 8) - (from_sq % 8)) 801 | if ( 802 | piece.piece == PAWN 803 | and file_diff == 1 804 | and board.board.piece_at(to_sq) is None 805 | ): 806 | flag = ENP_FLAG 807 | 808 | return cls(from_sq, to_sq, piece, flag) 809 | 810 | def promo_piece_type(self) -> Optional[Piece]: 811 | """Get the piece type for a promotion move. 812 | 813 | Returns: 814 | Optional[Piece]: The promotion piece type (KNIGHT, BISHOP, ROOK, QUEEN), 815 | or None if this is not a promotion move 816 | """ 817 | if self.is_promotion(): 818 | return (self.flag & 0x3) + KNIGHT 819 | else: 820 | return None 821 | 822 | def is_promotion(self) -> bool: 823 | """Check if this is a pawn promotion move. 824 | 825 | Returns: 826 | bool: True if this move is a pawn promotion 827 | """ 828 | return bool(self.flag & PROMO_FLAG) 829 | 830 | def is_enp(self) -> bool: 831 | """Check if this is an en passant capture. 832 | 833 | Returns: 834 | bool: True if this move is an en passant capture 835 | """ 836 | return bool(self.flag == ENP_FLAG) 837 | 838 | def is_castling(self) -> bool: 839 | """Check if this is a castling move. 840 | 841 | Returns: 842 | bool: True if this move is a castling move (kingside or queenside) 843 | """ 844 | return bool(self.flag == CASTLE_FLAG) 845 | 846 | @property 847 | def san(self) -> str: 848 | """The move in standard algebraic notation. 849 | 850 | Returns: 851 | str: Move in SAN format (e.g. 'e4', 'Nf3') 852 | """ 853 | return self.move_str() 854 | 855 | @classmethod 856 | def null(cls) -> Move: 857 | """Create a null move. 858 | 859 | Returns: 860 | Move: A move from square 0 to 0 with no piece or flags 861 | """ 862 | return cls(0, 0) 863 | 864 | 865 | class SquareSet: 866 | """Represents a set of squares on a chess board using a bitboard. 867 | 868 | A SquareSet uses a 64-bit integer (bitboard) where each bit represents a square 869 | on the chess board. This allows for efficient operations on sets of squares. 870 | 871 | The least significant bit represents square A1, and the most significant bit 872 | represents square H8: 873 | 874 | 8 | 56 57 58 59 60 61 62 63 875 | 7 | 48 49 50 51 52 53 54 55 876 | 6 | 40 41 42 43 44 45 46 47 877 | 5 | 32 33 34 35 36 37 38 39 878 | 4 | 24 25 26 27 28 29 30 31 879 | 3 | 16 17 18 19 20 21 22 23 880 | 2 | 8 9 10 11 12 13 14 15 881 | 1 | 0 1 2 3 4 5 6 7 882 | ------------------------- 883 | a b c d e f g h 884 | 885 | Attributes: 886 | mask (BitBoard): The underlying 64-bit integer representing the set of squares 887 | """ 888 | 889 | def __init__(self, mask: Optional[BitBoard] = 0): 890 | self.mask = 0 if mask is None else mask 891 | 892 | def __len__(self) -> int: 893 | """Return the number of squares in the set. 894 | 895 | Returns: 896 | Number of bits set to 1 in the bitboard 897 | """ 898 | return int(utils.popcount(self.mask)) 899 | 900 | def __str__(self) -> str: 901 | """Print an ASCII representation of the bitboard. 902 | 903 | Returns: 904 | Empty string after printing the board 905 | """ 906 | chess_lib.bb_print(self.mask) 907 | return "" 908 | 909 | def __repr__(self) -> str: 910 | return f"SquareSet({self.mask:#021_x})" 911 | 912 | def __add__(self, other: object) -> SquareSet: 913 | if not isinstance(other, SquareSet): 914 | return NotImplemented 915 | return SquareSet(self.mask | other.mask) 916 | 917 | def __and__(self, other: object) -> SquareSet: 918 | if not isinstance(other, SquareSet): 919 | return NotImplemented 920 | return SquareSet(other.mask & self.mask) 921 | 922 | def __xor__(self, other: object) -> SquareSet: 923 | if not isinstance(other, SquareSet): 924 | return NotImplemented 925 | return SquareSet(self.mask ^ other.mask) 926 | 927 | def __int__(self) -> int: 928 | return self.mask 929 | 930 | def __iter__(self) -> Iterator[Square]: 931 | return utils.scan_forward(self.mask) 932 | 933 | def __bool__(self) -> bool: 934 | return bool(self.mask) 935 | 936 | def __eq__(self, other: object) -> bool: 937 | if not isinstance(other, SquareSet): 938 | return NotImplemented 939 | return self.mask == other.mask 940 | 941 | def __contains__(self, sq: Square) -> bool: 942 | return bool(utils.bit(sq) & int(self)) 943 | 944 | def pop(self) -> Square: 945 | """Remove and return the least significant set square. 946 | 947 | Returns: 948 | Index of the least significant set bit 949 | 950 | Raises: 951 | KeyError: If the set is empty 952 | """ 953 | if not self.mask: 954 | raise KeyError("pop from empty SquareSet") 955 | square: Square = utils.get_lsb(self.mask) 956 | self.mask &= self.mask - 1 957 | return square 958 | 959 | def clear(self) -> None: 960 | """Remove all squares from the set.""" 961 | self.mask = BitBoard(0) 962 | 963 | def copy(self) -> SquareSet: 964 | """Create a copy of this SquareSet. 965 | 966 | Returns: 967 | New SquareSet with the same bits set 968 | """ 969 | return SquareSet(self.mask) 970 | 971 | def tolist(self) -> List[bool]: 972 | """Convert the bitboard to a list of boolean values. 973 | 974 | Returns: 975 | List of 64 booleans indicating which squares are set 976 | """ 977 | arr: List[bool] = [False] * SQUARE_NB 978 | for sq in self: 979 | arr[sq] = True 980 | return arr 981 | 982 | 983 | class Searcher: 984 | """Chess engine searcher that manages search parameters and execution.""" 985 | 986 | def __init__(self, board: Board, debug: bool = False): 987 | """Initialize the searcher with a board position. 988 | 989 | Args: 990 | board: The chess board to analyze 991 | debug: Enable debug output during search 992 | """ 993 | self.board = board 994 | self.search = Search() 995 | self.move = c_uint32() 996 | self.debug = debug 997 | self._is_searching = False 998 | 999 | def start(self, depth: int = 6, time_s: float = 1.0) -> Move: 1000 | """Start the search for the best move. 1001 | 1002 | Args: 1003 | depth: Maximum search depth (default 6 ply) 1004 | 1005 | Returns: 1006 | The best move found 1007 | 1008 | Raises: 1009 | ValueError: If depth is not positive 1010 | """ 1011 | if depth <= 0: 1012 | raise ValueError("Search depth must be positive") 1013 | 1014 | if self._is_searching: 1015 | raise RuntimeError("Search already in progress") 1016 | 1017 | self._is_searching = True 1018 | try: 1019 | # Initialize search thread 1020 | chess_lib.thread_init( 1021 | byref(self.search), 1022 | self.board.board.ptr, 1023 | byref(self.move), 1024 | time_s, 1025 | self.debug, 1026 | ) 1027 | 1028 | # Convert move value to Move object 1029 | best_move = self._convert_move(self.move.value) 1030 | 1031 | return best_move 1032 | 1033 | finally: 1034 | self._is_searching = False 1035 | 1036 | def stop(self) -> None: 1037 | """Stop the current search.""" 1038 | if self._is_searching: 1039 | # Stop the search 1040 | chess_lib.thread_stop(byref(self.search)) 1041 | self._is_searching = False 1042 | 1043 | def clear(self) -> None: 1044 | """Clear search state and hash tables.""" 1045 | self.search = Search() 1046 | self.move = c_uint32() 1047 | self._is_searching = False 1048 | 1049 | def _convert_move(self, move_val: int) -> Move: 1050 | """Convert raw move value to Move object.""" 1051 | if not move_val: 1052 | return Move.null() 1053 | 1054 | from_sq = (move_val >> 0) & 0x3F 1055 | to_sq = (move_val >> 6) & 0x3F 1056 | piece_val = (move_val >> 12) & 0xF 1057 | flag = (move_val >> 16) & 0xF 1058 | 1059 | piece = PieceType.from_index(piece_val) if piece_val != NONE else None 1060 | 1061 | return Move(from_sq, to_sq, piece, flag) 1062 | 1063 | @property 1064 | def nodes(self) -> int: 1065 | """Number of nodes searched.""" 1066 | nodes = self.search.nodes 1067 | return int(nodes) 1068 | 1069 | @property 1070 | def is_searching(self) -> bool: 1071 | """Whether a search is currently in progress.""" 1072 | return self._is_searching 1073 | 1074 | 1075 | class BaseBoard: 1076 | """A low-level chess board representation using bitboards. 1077 | 1078 | The BaseBoard class provides core functionality for representing and manipulating 1079 | a chess position using bitboards. Each piece type and color combination is 1080 | represented by a 64-bit integer where each bit corresponds to a square. 1081 | 1082 | This class handles: 1083 | - Piece placement and removal 1084 | - Board initialization 1085 | - Bitboard operations for pieces and squares 1086 | - Attack generation 1087 | - Position comparison and hashing 1088 | 1089 | The board is represented internally using the ChessBoard C structure through ctypes. 1090 | 1091 | Attributes: 1092 | baseboard (ChessBoard): Internal C structure representing the board state 1093 | """ 1094 | 1095 | def __init__(self) -> None: 1096 | self.baseboard: ChessBoard = ChessBoard() 1097 | self.board_init() 1098 | 1099 | def __repr__(self) -> str: 1100 | return f"{type(self).__name__}({self.occ(BOTH).mask:#021_x})" 1101 | 1102 | def __str__(self) -> str: 1103 | mask: BitBoard = self.occ(BOTH).mask 1104 | chess_lib.bb_print(mask) 1105 | return "" 1106 | 1107 | def __iter__(self) -> Iterator[Square]: 1108 | """Iterate through all occupied squares on the board. 1109 | 1110 | Yields: 1111 | Square: Index of each occupied square (0-63) 1112 | """ 1113 | mask: BitBoard = self.occ(BOTH).mask 1114 | return utils.scan_forward(mask) 1115 | 1116 | def __eq__(self, other: object) -> bool: 1117 | if not isinstance(other, BaseBoard): 1118 | return NotImplemented 1119 | 1120 | return ( 1121 | self.baseboard.castle == other.baseboard.castle 1122 | and self.baseboard.ep == other.baseboard.ep 1123 | and all(self.piece_at(sq) == other.piece_at(sq) for sq in range(SQUARE_NB)) 1124 | ) 1125 | 1126 | def board_init(self) -> None: 1127 | """Initialize the board's bitboards and other internal state. 1128 | 1129 | Must be called after creating a new board instance. 1130 | """ 1131 | chess_lib.bb_init() 1132 | chess_lib.board_init(self.ptr) 1133 | 1134 | def zobrist_key(self) -> BitBoard: 1135 | """Get the Zobrist hash key for the current position. 1136 | 1137 | The Zobrist key is a unique 64-bit hash of the current board state, 1138 | useful for position comparison and transposition tables. 1139 | 1140 | Returns: 1141 | BitBoard: 64-bit Zobrist hash value 1142 | """ 1143 | hash = self.baseboard.hash 1144 | return BitBoard(hash) 1145 | 1146 | def occ(self, color: Color) -> SquareSet: 1147 | """Get a bitboard of all pieces of a given color. 1148 | 1149 | Args: 1150 | color: WHITE (0), BLACK (1), or BOTH (2) 1151 | 1152 | Returns: 1153 | SquareSet: Bitboard of all squares occupied by pieces of the given color 1154 | 1155 | Raises: 1156 | ValueError: If color is not WHITE, BLACK, or BOTH 1157 | """ 1158 | if not isinstance(color, Color) or color < WHITE or color > BOTH: 1159 | raise ValueError( 1160 | f"Invalid color value: {color}. Must be WHITE (0), BLACK (1) or BOTH (2)" 1161 | ) 1162 | mask: BitBoard = self.baseboard.occ[color] 1163 | return SquareSet(mask) 1164 | 1165 | def _get_piece_squares(self, piece: Piece, color: Color) -> SquareSet: 1166 | if not isinstance(color, Color) or color < WHITE or color > BLACK: 1167 | raise ValueError( 1168 | f"Invalid color value: {color}. Must be WHITE (0), BLACK (1)" 1169 | ) 1170 | piece_type = PieceType(piece, BLACK if color else WHITE) 1171 | mask: BitBoard = self.baseboard.bb_squares[hash(piece_type)] 1172 | return SquareSet(mask) 1173 | 1174 | def pawns(self, color: Color) -> SquareSet: 1175 | return self._get_piece_squares(PAWN, color) 1176 | 1177 | def knights(self, color: Color) -> SquareSet: 1178 | return self._get_piece_squares(KNIGHT, color) 1179 | 1180 | def rooks(self, color: Color) -> SquareSet: 1181 | return self._get_piece_squares(ROOK, color) 1182 | 1183 | def bishops(self, color: Color) -> SquareSet: 1184 | return self._get_piece_squares(BISHOP, color) 1185 | 1186 | def queens(self, color: Color) -> SquareSet: 1187 | return self._get_piece_squares(QUEEN, color) 1188 | 1189 | def kings(self, color: Color) -> SquareSet: 1190 | return self._get_piece_squares(KING, color) 1191 | 1192 | def king_sq(self, color: Color) -> Square: 1193 | if not isinstance(color, Color) or color < WHITE or color > BOTH: 1194 | raise ValueError( 1195 | f"Invalid color value: {color}. Must be WHITE (0), BLACK (1) or BOTH (2)" 1196 | ) 1197 | mask: BitBoard = self.kings(color).mask 1198 | return utils.get_lsb(mask) 1199 | 1200 | def piece_at(self, square: Square) -> Optional[PieceType]: 1201 | """Get the piece at a given square. 1202 | 1203 | Args: 1204 | square: The square index (0-63) 1205 | 1206 | Returns: 1207 | Optional[PieceType]: The piece on the square, or None if empty 1208 | 1209 | Raises: 1210 | IndexError: If square is not in valid range 0-63 1211 | """ 1212 | if not square in range(SQUARE_NB): 1213 | raise IndexError 1214 | piece = self.baseboard.squares[square] 1215 | return PieceType.from_index(piece) 1216 | 1217 | def color_at(self, square: Square) -> Optional[Color]: 1218 | """Get the color of the piece at a given square. 1219 | 1220 | Args: 1221 | square: The square index (0-63) 1222 | 1223 | Returns: 1224 | Optional[Color]: WHITE (0) or BLACK (1) if a piece is present, 1225 | None if the square is empty 1226 | """ 1227 | mask: BitBoard = utils.bit(square) 1228 | if self.occ(WHITE).mask & mask: 1229 | return WHITE 1230 | elif self.occ(BLACK).mask & mask: 1231 | return BLACK 1232 | else: 1233 | return None 1234 | 1235 | def attacks_mask(self, square: Square) -> BitBoard: 1236 | mask: BitBoard = chess_lib.attacks_to_square( 1237 | self.ptr, square, self.occ(BOTH).mask 1238 | ) 1239 | return mask 1240 | 1241 | def attacks(self, color: Color, square: Square) -> SquareSet: 1242 | """Get all pieces of a given color that attack a square. 1243 | 1244 | Args: 1245 | color: The attacking color (WHITE or BLACK) 1246 | square: The target square being attacked 1247 | 1248 | Returns: 1249 | SquareSet: Bitboard of attacking pieces of the given color 1250 | """ 1251 | mask: BitBoard = self.attacks_mask(square) 1252 | return SquareSet(mask) & self.occ(color) 1253 | 1254 | def castling_rights(self) -> int: 1255 | rights = self.baseboard.castle 1256 | return int(rights) 1257 | 1258 | def clear(self) -> None: 1259 | chess_lib.board_clear(self.ptr) 1260 | 1261 | def tolist(self) -> List[Optional[PieceType]]: 1262 | arr: List[Optional[PieceType]] = [None] * SQUARE_NB 1263 | for sq in self: 1264 | arr[sq] = self.piece_at(sq) 1265 | return arr 1266 | 1267 | def drawn_by_insufficient_material(self) -> bool: 1268 | return bool(chess_lib.board_drawn_by_insufficient_material(self.ptr)) 1269 | 1270 | def unicode_print(self) -> None: 1271 | chess_lib.print_board(self.ptr) 1272 | return None 1273 | 1274 | def board_to_fen(self) -> str: 1275 | buffer: Array[c_char] = utils.create_string_buffer(256) 1276 | chess_lib.board_to_fen(self.ptr, buffer) 1277 | return buffer.value.decode("utf-8") 1278 | 1279 | def set_fen(self, fen: str) -> None: 1280 | chess_lib.board_load_fen(self.ptr, fen.encode()) 1281 | 1282 | def is_check(self) -> bool: 1283 | return bool(chess_lib.is_check(self.ptr)) 1284 | 1285 | def illegal_to_move(self) -> bool: 1286 | return bool(chess_lib.illegal_to_move(self.ptr)) 1287 | 1288 | def perft(self, depth: Optional[int] = 1) -> int: 1289 | nodes = chess_lib.perft_test(self.ptr, depth) 1290 | return int(nodes) 1291 | 1292 | def turn(self) -> Color: 1293 | color = self.baseboard.color 1294 | return Color(color) 1295 | 1296 | @property 1297 | def ptr(self) -> object: 1298 | return byref(self.baseboard) 1299 | 1300 | 1301 | class MoveUndo: 1302 | def __init__(self, move: Move = Move.null()): 1303 | self._move = move 1304 | self.undo = Undo() 1305 | 1306 | @property 1307 | def piece(self) -> Optional[PieceType]: 1308 | """Get the piece that made the move. 1309 | 1310 | Returns: 1311 | Optional[PieceType]: The piece type and color, or None if null move 1312 | """ 1313 | if self._move: 1314 | return PieceType.from_index( 1315 | hash(self._move.piece) if self._move.piece else 0 1316 | ) 1317 | return None 1318 | 1319 | @property 1320 | def capture(self) -> Optional[PieceType]: 1321 | """Get the piece that was captured by this move. 1322 | 1323 | Returns: 1324 | Optional[PieceType]: The captured piece type and color, or None if no capture 1325 | """ 1326 | capture = self.undo.capture 1327 | return PieceType.from_index(capture) 1328 | 1329 | @property 1330 | def enp_square(self) -> Optional[Square]: 1331 | """Get the en passant square if this was an en passant capture. 1332 | 1333 | Returns: 1334 | Optional[Square]: The square index (0-63) of the captured pawn in an en passant move, 1335 | or None if not an en passant capture 1336 | """ 1337 | if self._move and self._move.is_enp(): 1338 | return utils.get_lsb(self.undo.ep) 1339 | return None 1340 | 1341 | @property 1342 | def ptr(self) -> Any: 1343 | return byref(self.undo) 1344 | 1345 | def __repr__(self) -> str: 1346 | return ( 1347 | f"{type(self).__name__}(san={self._move.san}, " 1348 | f"piece={self.piece}, capture={self.capture}, " 1349 | f"enp_square={self.enp_square})" 1350 | ) 1351 | 1352 | def __eq__(self, other: object) -> bool: 1353 | if isinstance(other, MoveUndo): 1354 | return self._move == other._move 1355 | return False 1356 | 1357 | 1358 | class Board: 1359 | """A chess board representation with move generation and validation. 1360 | 1361 | The Board class provides a high-level interface for chess game mechanics including: 1362 | - Move generation and validation 1363 | - Position tracking and manipulation 1364 | - Game state checking (checkmate, stalemate, etc.) 1365 | - FEN string parsing and generation 1366 | - Board position display 1367 | 1368 | The board maintains the complete game state including: 1369 | - Piece positions (via BaseBoard) 1370 | - Side to move 1371 | - Move history 1372 | - Castling rights 1373 | - En passant square 1374 | - Move counters 1375 | 1376 | Attributes: 1377 | board (BaseBoard): Low-level bitboard representation 1378 | _handle_moves (MovesStack): Stack of moves and their undo information 1379 | 1380 | Example: 1381 | >>> board = Board() # Create new board with starting position 1382 | >>> board.unicode() # Display board with Unicode pieces 1383 | >>> legal_moves = list(board.gen_legal_moves) # Get legal moves 1384 | >>> board.push(legal_moves[0]) # Make a move 1385 | >>> board.pop() # Take back the move 1386 | """ 1387 | 1388 | def __init__(self, fen: Optional[str] = None): 1389 | self.board: BaseBoard = BaseBoard() 1390 | if fen is None: 1391 | self.set_fen(STARTING_FEN) 1392 | else: 1393 | self.set_fen(fen) 1394 | self._handle_moves: MovesStack = MovesStack(self) 1395 | 1396 | def drawn_by_insufficient_material(self) -> bool: 1397 | """Check if the position is drawn due to insufficient material. 1398 | 1399 | Returns: 1400 | bool: True if neither side has sufficient material to checkmate 1401 | """ 1402 | return self.board.drawn_by_insufficient_material() 1403 | 1404 | def clear(self) -> None: 1405 | """Reset the board to starting position and clear move history.""" 1406 | self.board.clear() 1407 | self._handle_moves.clear() 1408 | self.set_fen(fen=STARTING_FEN) 1409 | 1410 | def castling_rights(self) -> int: 1411 | 1412 | return self.board.castling_rights() 1413 | 1414 | def _generate_moves(self, func_name: str) -> Iterator[Move]: 1415 | array: Array[Any] = utils.create_uint32_array(MAX_MOVES) 1416 | size: int = getattr(chess_lib, func_name)(self.board.ptr, array) 1417 | for data in utils.scan_move_list(array[:size]): 1418 | yield Move(*data) 1419 | 1420 | def generate_pseudo_legal_moves(self) -> Iterator[Move]: 1421 | return self._generate_moves("gen_moves") 1422 | 1423 | def generate_legal_moves(self) -> Iterator[Move]: 1424 | return self._generate_moves("gen_legal_moves") 1425 | 1426 | @property 1427 | def turn(self) -> Color: 1428 | """Get the turn color '0' for WHITE, '1' for BLACK""" 1429 | return self.board.turn() 1430 | 1431 | @property 1432 | def gen_legal_moves(self) -> LegalMoveGenerator: 1433 | return LegalMoveGenerator(self) 1434 | 1435 | @property 1436 | def gen_pseudo_legal_moves(self) -> PseudoLegalMoveGenerator: 1437 | return PseudoLegalMoveGenerator(self) 1438 | 1439 | @property 1440 | def fen(self) -> str: 1441 | """Get FEN string representation of current position. 1442 | 1443 | Returns: 1444 | str: The position in Forsyth–Edwards Notation 1445 | """ 1446 | return self.board.board_to_fen() 1447 | 1448 | def set_fen(self, fen: str) -> None: 1449 | """Set the board position from a FEN string. 1450 | 1451 | Args: 1452 | fen: Position in Forsyth–Edwards Notation 1453 | 1454 | Raises: 1455 | TypeError: If fen is not a string 1456 | ValueError: If fen is empty or invalid 1457 | """ 1458 | if not isinstance(fen, str): 1459 | raise TypeError("FEN must be a string") 1460 | if not fen: 1461 | raise ValueError("FEN cannot be empty") 1462 | self.board.set_fen(fen) 1463 | 1464 | def perft_test(self, depth: int = 1) -> BitBoard: 1465 | """Calculate number of legal moves at given depth (performance test). 1466 | 1467 | Args: 1468 | depth: Search depth (must be positive) 1469 | 1470 | Returns: 1471 | BitBoard: Number of legal positions at given depth 1472 | 1473 | Raises: 1474 | ValueError: If depth is not positive 1475 | """ 1476 | if depth <= 0: 1477 | raise ValueError("Depth must be non-negative") 1478 | return self.board.perft(depth) 1479 | 1480 | def color_at(self, square: Square) -> Optional[Color]: 1481 | """Get the color of the piece at given square. 1482 | 1483 | Args: 1484 | square: Square index (0-63) 1485 | 1486 | Returns: 1487 | Optional[Color]: WHITE, BLACK, or None if empty 1488 | """ 1489 | return self.board.color_at(square) 1490 | 1491 | def is_game_over(self) -> bool: 1492 | """Check if the game is over. 1493 | 1494 | Returns: 1495 | bool: True if the game is over by any means 1496 | (checkmate, stalemate, insufficient material, etc.) 1497 | """ 1498 | return ( 1499 | self.is_checkmate() 1500 | or self.is_stalemate() 1501 | or self.drawn_by_insufficient_material() 1502 | or self.is_fifty_moves() 1503 | or self.is_threefold_repetition() 1504 | ) 1505 | 1506 | def is_checkmate(self) -> bool: 1507 | """Check if the position is checkmate. 1508 | 1509 | Returns: 1510 | bool: True if the side to move is in checkmate 1511 | """ 1512 | return self.is_check() and not any(self.gen_legal_moves) 1513 | 1514 | def is_fifty_moves(self) -> bool: 1515 | return False 1516 | 1517 | def is_threefold_repetition(self) -> bool: 1518 | return False 1519 | 1520 | def is_stalemate(self) -> bool: 1521 | """Check if the position is stalemate. 1522 | 1523 | Returns: 1524 | bool: True if the side to move has no legal moves but is not in check 1525 | """ 1526 | return not self.is_check() and not any(self.gen_legal_moves) 1527 | 1528 | def is_check(self) -> bool: 1529 | """Check if the side to move is in check. 1530 | 1531 | Returns: 1532 | bool: True if the current side's king is under attack 1533 | """ 1534 | return self.board.is_check() 1535 | 1536 | def illegal_to_move(self) -> bool: 1537 | return self.board.illegal_to_move() 1538 | 1539 | def checkers(self, color: Color) -> SquareSet: 1540 | """Get pieces giving check to a king. 1541 | 1542 | Args: 1543 | color: Color of the king being checked 1544 | 1545 | Returns: 1546 | SquareSet: Bitboard of all pieces attacking the specified king 1547 | 1548 | Exemple: 1549 | >>> board = Board("r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4") 1550 | >>> 1551 | >>> checkers = board.checkers(BLACK) 1552 | >>> print(checkers) 1553 | . . . . . . . . 1554 | . . . . . 1 . . 1555 | . . . . . . . . 1556 | . . . . . . . . 1557 | . . . . . . . . 1558 | . . . . . . . . 1559 | . . . . . . . . 1560 | . . . . . . . . 1561 | """ 1562 | king_sq: int = self.board.king_sq(color) 1563 | return self.attackers(not color, king_sq) 1564 | 1565 | def is_square_attacked_by(self, color: Color, square: Square) -> bool: 1566 | """Check if a square is attacked by any piece of the given color. 1567 | 1568 | Args: 1569 | color: The attacking color (WHITE=0 or BLACK=1) 1570 | square: The target square index (0-63) to check for attacks 1571 | 1572 | Returns: 1573 | bool: True if the square is attacked by any piece of the specified color 1574 | 1575 | Raises: 1576 | ValueError: If square is not a valid square index (0-63) 1577 | 1578 | Example: 1579 | >>> board = Board("8/8/8/3k4/4Q3/8/8/3K4 w - - 0 1") 1580 | >>> # Check if black king on d5 is attacked by white pieces 1581 | >>> board.is_square_attacked_by(WHITE, D5) 1582 | True 1583 | """ 1584 | if not isinstance(square, int) or not (0 <= square < SQUARE_NB): 1585 | raise ValueError(f"Invalid square index: {square}") 1586 | return bool(self.board.attacks(color, square)) 1587 | 1588 | def attackers(self, color: Color, square: Square) -> SquareSet: 1589 | """Get all pieces of a given color that attack a specific square. 1590 | 1591 | This method returns a bitboard containing all pieces of the specified color 1592 | that attack the given square in the current position. 1593 | 1594 | Args: 1595 | color: The attacking color (WHITE=0 or BLACK=1) 1596 | square: The target square index (0-63) being attacked 1597 | 1598 | Returns: 1599 | SquareSet: Bitboard of all pieces of the given color that attack the square 1600 | 1601 | Raises: 1602 | ValueError: If square is not a valid square index (0-63) 1603 | 1604 | Examples: 1605 | >>> board = Board("8/8/8/3k4/4Q3/8/8/3K4 w - - 0 1") 1606 | >>> attackers = board.attackers(WHITE, D5) # Find white pieces attacking d5 1607 | >>> print(attackers) # Shows queen attacking black king 1608 | . . . . . . . . 1609 | . . . . . . . . 1610 | . . . . . . . . 1611 | . . . . . . . . 1612 | . . . . 1 . . . 1613 | . . . . . . . . 1614 | . . . . . . . . 1615 | . . . . . . . . 1616 | """ 1617 | if not isinstance(square, int) or not (0 <= square < SQUARE_NB): 1618 | raise ValueError(f"Invalid square index: {square}") 1619 | return self.board.attacks(color, square) 1620 | 1621 | def push(self, move: Move) -> None: 1622 | """Make a move on the board. 1623 | 1624 | Args: 1625 | move: The move to make 1626 | 1627 | Raises: 1628 | IllegalMoveError: If the move is not legal in current position 1629 | 1630 | Example: 1631 | >>> 1632 | >>> board = Board() 1633 | >>> move = Move(E2, E3, PieceType(PAWN, WHITE)) 1634 | >>> board.push(move) # Make the move 1635 | >>> board.pop() # Unmake the last move 1636 | Move(san=e2e3, from=12, to=20, piece=PieceType'p', flag=0) 1637 | """ 1638 | self._handle_moves.push(move) 1639 | 1640 | def pop(self) -> Move: 1641 | """Take back the last move made. 1642 | 1643 | Returns: 1644 | Move: The move that was undone 1645 | 1646 | Raises: 1647 | IndexError: If there are no moves to undo 1648 | """ 1649 | return self._handle_moves.pop() 1650 | 1651 | def push_null(self) -> None: 1652 | """Push a null move this will just flip the color 1653 | and update the zobrist hash. 1654 | """ 1655 | return None 1656 | 1657 | def pop_null(self) -> Move: 1658 | return NotImplemented 1659 | 1660 | def peek(self) -> Optional[Move]: 1661 | return self._handle_moves.peek() 1662 | 1663 | def copy(self) -> Self: 1664 | dst = type(self)(None) 1665 | pointer(dst.board.baseboard)[ 1666 | 0 1667 | ] = ( 1668 | self.board.baseboard 1669 | ) # https://stackoverflow.com/questions/1470343/python-copying-structures-contents 1670 | 1671 | dst._handle_moves = self._handle_moves.copy(dst) 1672 | return dst 1673 | 1674 | def __copy__(self) -> Self: 1675 | return self.copy() 1676 | 1677 | def __eq__(self, other: object) -> bool: 1678 | if not isinstance(other, Board): 1679 | return NotImplemented 1680 | return ( 1681 | self.fen == other.fen 1682 | and self._handle_moves == other._handle_moves 1683 | and self.board == other.board 1684 | ) 1685 | 1686 | def __repr__(self) -> str: 1687 | return f"{type(self).__name__}(fen={self.fen!r})" 1688 | 1689 | def __str__(self) -> str: 1690 | """ 1691 | Returns a string representation of the board with ranks and files. 1692 | 1693 | >>> import sisyphus 1694 | >>> 1695 | >>> board = sisyphus.Board() 1696 | >>> print (board) 1697 | 1698 | 8 R N B Q K B N R 1699 | 7 P P P P P P P P 1700 | 6 . . . . . . . . 1701 | 5 . . . . . . . . 1702 | 4 . . . . . . . . 1703 | 3 . . . . . . . . 1704 | 2 p p p p p p p p 1705 | 1 r n b q k b n r 1706 | a b c d e f g h 1707 | 1708 | """ 1709 | builder: List[str] = [] 1710 | 1711 | for rank in range(7, -1, -1): 1712 | builder.append(f"{rank + 1} ") 1713 | 1714 | for file in range(8): 1715 | square = utils.square(rank, file) 1716 | piece = self.board.piece_at(square) 1717 | 1718 | if piece: 1719 | builder.append(piece.symbol()) 1720 | else: 1721 | builder.append(".") 1722 | builder.append(" ") 1723 | 1724 | builder.append("\n") 1725 | 1726 | builder.append(" a b c d e f g h") 1727 | 1728 | return "".join(builder) 1729 | 1730 | def unicode(self) -> None: 1731 | """ 1732 | pretty-printing the current board position using Unicode chess symbols. 1733 | 1734 | >>> 1735 | >>> board = sisyphus.Board() 1736 | >>> board.unicode() 1737 | 1738 | 8 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 1739 | 7 ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 1740 | 6 . . . . . . . . 1741 | 5 . . . . . . . . 1742 | 4 . . . . . . . . 1743 | 3 . . . . . . . . 1744 | 2 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 1745 | 1 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 1746 | a b c d e f g h 1747 | 1748 | 1749 | rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1750 | """ 1751 | self.board.unicode_print() 1752 | 1753 | def __hash__(self) -> int: 1754 | return self.board.zobrist_key() 1755 | 1756 | 1757 | class PseudoLegalMoveGenerator: 1758 | """Generator for pseudo legal moves in a given position.""" 1759 | 1760 | def __init__(self, board: Board): 1761 | self.board = board 1762 | 1763 | def __bool__(self) -> bool: 1764 | return any(self.board.generate_pseudo_legal_moves()) 1765 | 1766 | def __len__(self) -> int: 1767 | return len(list(self)) 1768 | 1769 | def __iter__(self) -> Iterator[Move]: 1770 | return self.board.generate_pseudo_legal_moves() 1771 | 1772 | def __contains__(self, move: Move) -> bool: 1773 | return move in set( 1774 | self 1775 | ) # TODO: add a function that check if a move is psaudo legal 1776 | 1777 | def __repr__(self) -> str: 1778 | sans = ", ".join(m.san for m in self) 1779 | return f"{type(self).__name__} at {id(self):#x} ({sans})" 1780 | 1781 | 1782 | class LegalMoveGenerator: 1783 | """Generator for legal moves in a given position.""" 1784 | 1785 | def __init__(self, board: Board): 1786 | self.board = board 1787 | 1788 | def __bool__(self) -> bool: 1789 | return any(self.board.generate_legal_moves()) 1790 | 1791 | def __iter__(self) -> Iterator[Move]: 1792 | return self.board.generate_legal_moves() 1793 | 1794 | def __contains__(self, move: Move) -> bool: 1795 | return move in set( 1796 | self 1797 | ) # TODO: add a function that check if a move is a legal move 1798 | 1799 | def __repr__(self) -> str: 1800 | sans = ", ".join(m.san for m in self) 1801 | return f"{type(self).__name__} at {id(self):#x} ({sans})" 1802 | 1803 | 1804 | class MovesStack: 1805 | """A simple stack data structure that keeps track of chess moves and their undo information.""" 1806 | 1807 | def __init__(self, board: Board) -> None: 1808 | self.board = board 1809 | self._moves_history: List[Tuple[Move, MoveUndo]] = [] 1810 | 1811 | def push(self, move: object) -> None: 1812 | if not isinstance(move, Move): 1813 | raise TypeError(f"Expected Move instance, got {type(move).__name__}") 1814 | 1815 | # NOTE: we don' t check for move legality. 1816 | # if not move in LegalMoveGenerator(self.board): 1817 | # raise IllegalMoveError( 1818 | # f"Move {move.san} is not legal in the current position." 1819 | # ) 1820 | 1821 | undo: MoveUndo = MoveUndo(move) 1822 | chess_lib.do_move(self.board.board.ptr, hash(move), undo.ptr) 1823 | self._moves_history.append((move, undo)) 1824 | 1825 | def pop(self) -> Move: 1826 | if len(self): 1827 | move, undo = self._moves_history.pop() 1828 | else: 1829 | raise IndexError("pop from empty move history") 1830 | 1831 | chess_lib.undo_move(self.board.board.ptr, hash(move), undo.ptr) 1832 | return move 1833 | 1834 | def peek(self) -> Optional[Move]: 1835 | return self._moves_history[-1][0] if self._moves_history else None 1836 | 1837 | def clear(self) -> None: 1838 | self._moves_history.clear() 1839 | 1840 | def copy(self, other: Board) -> Self: 1841 | dst = type(self)(other) 1842 | dst._moves_history = self._moves_history.copy() 1843 | return dst 1844 | 1845 | def __eq__(self, other: object) -> bool: 1846 | if not isinstance(other, MovesStack): 1847 | return NotImplemented 1848 | return self._moves_history == other._moves_history 1849 | 1850 | def __len__(self) -> int: 1851 | return len(self._moves_history) 1852 | 1853 | def __iter__(self) -> Iterator[Tuple[Move, MoveUndo]]: 1854 | return ((mv, undo) for mv, undo in self._moves_history) 1855 | 1856 | def __contains__(self, move: Move) -> bool: 1857 | return any(mv == move for mv, _ in self) 1858 | 1859 | def __repr__(self) -> str: 1860 | moves = ", ".join(mv.san for mv, _ in self) 1861 | return f"<{type(self).__name__} id={id(self):#x} moves=[{moves}]>" 1862 | -------------------------------------------------------------------------------- /sisyphus/attacks.c: -------------------------------------------------------------------------------- 1 | #include "attacks.h" 2 | 3 | INLINE bb get_pawns_attacks(int sq, int color) { 4 | assert(sq >= 0 && sq < SQUARE_NB); 5 | assert(color == WHITE || color == BLACK); 6 | return BB_PAWNS[color][sq]; 7 | } 8 | 9 | INLINE bb get_bishop_attacks(int sq, bb obs) { 10 | assert(sq >= 0 && sq < SQUARE_NB); 11 | return bb_bishop(sq, obs); 12 | } 13 | 14 | INLINE bb get_knight_attacks(int sq) { 15 | assert(sq >= 0 && sq < SQUARE_NB); 16 | return BB_KNIGHT[sq]; 17 | } 18 | 19 | INLINE bb get_rook_attacks(int sq, bb obs) { 20 | assert(sq >= 0 && sq < SQUARE_NB); 21 | return bb_rook(sq, obs); 22 | } 23 | 24 | INLINE bb get_queen_attacks(int sq, bb obs) { 25 | assert(sq >= 0 && sq < SQUARE_NB); 26 | return bb_queen(sq, obs); 27 | } 28 | 29 | INLINE bb get_king_attacks(int sq) { 30 | assert(sq >= 0 && sq < SQUARE_NB); 31 | return BB_KING[sq]; 32 | } 33 | 34 | int attacks_to_king_square(ChessBoard *board, const bb b_king) { 35 | assert(b_king); 36 | return (attacks_to_square(board, get_lsb(b_king), board->occ[BOTH]) & 37 | board->occ[board->color]) 38 | ? 1 39 | : 0; 40 | } 41 | 42 | bb attacks_to_square(ChessBoard *board, int sq, bb occ) { 43 | return ((get_pawns_attacks(sq, WHITE) & (board->bb_squares[BLACK_PAWN])) | 44 | (get_pawns_attacks(sq, BLACK) & (board->bb_squares[WHITE_PAWN])) | 45 | (get_knight_attacks(sq) & (board->bb_squares[WHITE_KNIGHT] | 46 | board->bb_squares[BLACK_KNIGHT])) | 47 | (get_bishop_attacks(sq, occ) & (board->bb_squares[WHITE_BISHOP] | 48 | board->bb_squares[BLACK_BISHOP])) | 49 | (get_rook_attacks(sq, occ) & 50 | (board->bb_squares[WHITE_ROOK] | board->bb_squares[BLACK_ROOK])) | 51 | (get_queen_attacks(sq, occ) & 52 | (board->bb_squares[WHITE_QUEEN] | board->bb_squares[BLACK_QUEEN])) | 53 | (get_king_attacks(sq) & 54 | (board->bb_squares[WHITE_KING] | board->bb_squares[BLACK_KING]))); 55 | } -------------------------------------------------------------------------------- /sisyphus/attacks.h: -------------------------------------------------------------------------------- 1 | #ifndef ATTACKS_H 2 | #define ATTACKS_H 3 | 4 | #include "bb.h" 5 | #include "types.h" 6 | 7 | bb get_bishop_attacks(int sq, bb obs); 8 | 9 | bb get_rook_attacks(int sq, bb obs); 10 | 11 | int attacks_to_king_square(ChessBoard *board, bb b_king); 12 | 13 | bb attacks_to_square(ChessBoard *board, int sq, bb occ); 14 | 15 | #endif // ATTACKS_H -------------------------------------------------------------------------------- /sisyphus/bb.c: -------------------------------------------------------------------------------- 1 | #include "bb.h" 2 | 3 | bb BB_PAWNS[2][64]; 4 | bb BB_KNIGHT[64]; 5 | bb BB_BISHOP[64]; 6 | bb BB_ROOK[64]; 7 | bb BB_KING[64]; 8 | 9 | const bb MAGIC_BISHOP[64] = { 10 | 0x010a0a1023020080L, 0x0050100083024000L, 0x8826083200800802L, 11 | 0x0102408100002400L, 0x0414242008000000L, 0x0414242008000000L, 12 | 0x0804230108200880L, 0x0088840101012000L, 0x0400420202041100L, 13 | 0x0400420202041100L, 0x1100300082084211L, 0x0000124081000000L, 14 | 0x0405040308000411L, 0x01000110089c1008L, 0x0030108805101224L, 15 | 0x0010808041101000L, 0x2410002102020800L, 0x0010202004098180L, 16 | 0x1104000808001010L, 0x274802008a044000L, 0x1400884400a00000L, 17 | 0x0082000048260804L, 0x4004840500882043L, 0x0081001040680440L, 18 | 0x4282180040080888L, 0x0044200002080108L, 0x2404c80a04002400L, 19 | 0x2020808028020002L, 0x0129010050304000L, 0x0008020108430092L, 20 | 0x005600450c884800L, 0x005600450c884800L, 0x001004501c200301L, 21 | 0xa408025880100100L, 0x1042080300060a00L, 0x4100a00801110050L, 22 | 0x11240100c40c0040L, 0x24a0281141188040L, 0x08100c4081030880L, 23 | 0x020c310201002088L, 0x006401884600c280L, 0x1204028210809888L, 24 | 0x8000a01402005002L, 0x041d8a021a000400L, 0x041d8a021a000400L, 25 | 0x000201a102004102L, 0x0408010842041282L, 0x000201a102004102L, 26 | 0x0804230108200880L, 0x0804230108200880L, 0x8001010402090010L, 27 | 0x0008000042020080L, 0x4200012002440000L, 0x80084010228880a0L, 28 | 0x4244049014052040L, 0x0050100083024000L, 0x0088840101012000L, 29 | 0x0010808041101000L, 0x1090c00110511001L, 0x2124000208420208L, 30 | 0x0800102118030400L, 0x0010202120024080L, 0x00024a4208221410L, 31 | 0x010a0a1023020080L 32 | }; 33 | 34 | const bb MAGIC_ROOK[64] = { 35 | 0x0080004000608010L, 0x2240100040012002L, 0x008008a000841000L, 36 | 0x0100204900500004L, 0x020008200200100cL, 0x40800c0080020003L, 37 | 0x0080018002000100L, 0x4200042040820d04L, 0x10208008a8400480L, 38 | 0x4064402010024000L, 0x2181002000c10212L, 0x5101000850002100L, 39 | 0x0010800400080081L, 0x0012000200300815L, 0x060200080e002401L, 40 | 0x4282000420944201L, 0x1040208000400091L, 0x0010004040002008L, 41 | 0x0082020020804011L, 0x0005420010220208L, 0x8010510018010004L, 42 | 0x05050100088a1400L, 0x0009008080020001L, 0x2001060000408c01L, 43 | 0x0060400280008024L, 0x9810401180200382L, 0x0200201200420080L, 44 | 0x0280300100210048L, 0x0000080080800400L, 0x0002010200081004L, 45 | 0x8089000900040200L, 0x0040008200340047L, 0x0400884010800061L, 46 | 0xc202401000402000L, 0x0800401301002004L, 0x4c43502042000a00L, 47 | 0x0004a80082800400L, 0xd804040080800200L, 0x060200080e002401L, 48 | 0x0203216082000104L, 0x0000804000308000L, 0x004008100020a000L, 49 | 0x1001208042020012L, 0x0400220088420010L, 0x8010510018010004L, 50 | 0x8009000214010048L, 0x6445006200130004L, 0x000a008402460003L, 51 | 0x0080044014200240L, 0x0040012182411500L, 0x0003102001430100L, 52 | 0x4c43502042000a00L, 0x1008000400288080L, 0x0806003008040200L, 53 | 0x4200020801304400L, 0x8100640912804a00L, 0x300300a043168001L, 54 | 0x0106610218400081L, 0x008200c008108022L, 0x0201041861017001L, 55 | 0x00020010200884e2L, 0x0205000e18440001L, 0x202008104a08810cL, 56 | 0x800a208440230402L 57 | }; 58 | 59 | const int SHIFT_BISHOP[64] = { 60 | 58, 59, 59, 59, 59, 59, 59, 58, 59, 59, 59, 59, 59, 59, 59, 59, 61 | 59, 59, 57, 57, 57, 57, 59, 59, 59, 59, 57, 55, 55, 57, 59, 59, 62 | 59, 59, 57, 55, 55, 57, 59, 59, 59, 59, 57, 57, 57, 57, 59, 59, 63 | 59, 59, 59, 59, 59, 59, 59, 59, 58, 59, 59, 59, 59, 59, 59, 58 64 | }; 65 | 66 | const int SHIFT_ROOK[64] = { 67 | 52, 53, 53, 53, 53, 53, 53, 52, 53, 54, 54, 54, 54, 54, 54, 53, 68 | 53, 54, 54, 54, 54, 54, 54, 53, 53, 54, 54, 54, 54, 54, 54, 53, 69 | 53, 54, 54, 54, 54, 54, 54, 53, 53, 54, 54, 54, 54, 54, 54, 53, 70 | 53, 54, 54, 54, 54, 54, 54, 53, 52, 53, 53, 53, 53, 53, 53, 52, 71 | }; 72 | 73 | int OFFSET_BISHOP[64]; 74 | int OFFSET_ROOK[64]; 75 | 76 | bb ATTACK_BISHOP[5248]; 77 | bb ATTACK_ROOK[102400]; 78 | 79 | int get_lsb(bb bbit) { 80 | // https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fclz 81 | // If bbit is 0, the result is undefined 82 | assert(bbit); 83 | return __builtin_ctzll(bbit); 84 | } 85 | 86 | int get_msb(bb bbit) { 87 | assert(bbit); 88 | return __builtin_ctzll(bbit) ^ 63; 89 | } 90 | 91 | int popcount(bb bbit) { 92 | return __builtin_popcountll(bbit); 93 | } 94 | 95 | int several(bb bbit) { 96 | return bbit & (bbit - 1); 97 | } 98 | 99 | bool test_bit(bb bbit, const int sq) { 100 | assert(sq >= 0 && sq < SQUARE_NB); 101 | return (bool)(bbit & BIT(sq)); 102 | } 103 | 104 | int square(int rank, int file) { 105 | assert(0 <= rank && rank < RANK_NB); 106 | assert(0 <= file && file < FILE_NB); 107 | return rank * FILE_NB + file; 108 | } 109 | 110 | int file_of(int sq) { 111 | assert(0 <= sq && sq < SQUARE_NB); 112 | return sq % FILE_NB; 113 | } 114 | 115 | int rank_of(int sq) { 116 | assert(0 <= sq && sq < SQUARE_NB); 117 | return sq / RANK_NB; 118 | } 119 | 120 | int make_piece_type(int pc, int color) { 121 | assert(color == WHITE || color == BLACK); 122 | assert(pc >= PAWN && pc <= KING); 123 | return (pc << 1) + color; 124 | } 125 | 126 | int bb_squares(bb value, int squares[64]) { 127 | int i = 0; 128 | int sq; 129 | while (value) { 130 | POP_LSB(sq, value); 131 | squares[i++] = sq; 132 | } 133 | return i; 134 | } 135 | 136 | bb bb_pawns_attacks(int sq, int color) { 137 | assert(sq >= 0 && sq < SQUARE_NB); 138 | const bb board = BIT(sq); 139 | return color ? ((board & ~FILE_H) >> 7) | ((board & ~FILE_A) >> 9) 140 | : ((board & ~FILE_A) << 7) | ((board & ~FILE_H) << 9); 141 | } 142 | 143 | bb bb_slide(int sq, int truncate, bb obs, int directions[4][2]) { 144 | bb value = 0; 145 | int rank = sq / 8; 146 | int file = sq % 8; 147 | int i, n; 148 | for (i = 0; i < 4; i++) { 149 | bb prev = 0; 150 | for (n = 1; n < 9; n++) { 151 | int r = rank + directions[i][0] * n; 152 | int f = file + directions[i][1] * n; 153 | if (r < 0 || f < 0 || r > 7 || f > 7) { 154 | if (truncate) 155 | value &= ~prev; 156 | break; 157 | } 158 | bb bit = BIT(square(r, f)); 159 | value |= bit; 160 | if (bit & obs) 161 | break; 162 | prev = bit; 163 | } 164 | } 165 | 166 | return value; 167 | } 168 | 169 | bb bb_slide_bishop(int sq, int truncate, bb obs) { 170 | int directions[4][2] = {{-1, -1}, {-1, 1}, {1, -1}, {1, 1}}; 171 | 172 | return bb_slide(sq, truncate, obs, directions); 173 | } 174 | 175 | bb bb_slide_rook(int sq, int truncate, bb obs) { 176 | int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; 177 | return bb_slide(sq, truncate, obs, directions); 178 | } 179 | 180 | void bb_init() { 181 | // bb_pawns 182 | for (int sq = 0; sq < 64; sq++) { 183 | BB_PAWNS[WHITE][sq] = bb_pawns_attacks(sq, WHITE); 184 | BB_PAWNS[BLACK][sq] = bb_pawns_attacks(sq, BLACK); 185 | } 186 | 187 | // bb_knight 188 | const int knight_offsets[8][2] = { 189 | {-2, -1}, {-2, 1}, {2, -1}, {2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, 190 | }; 191 | 192 | for (int rank = 0; rank < 8; rank++) { 193 | for (int file = 0; file < 8; file++) { 194 | bb value = 0; 195 | for (int i = 0; i < 8; i++) { 196 | int r = rank + knight_offsets[i][0]; 197 | int f = file + knight_offsets[i][1]; 198 | if (r >= 0 && f >= 0 && r < 8 && f < 8) { 199 | value |= BIT(square(r, f)); 200 | } 201 | } 202 | BB_KNIGHT[square(rank, file)] = value; 203 | } 204 | } 205 | 206 | // bb_bishop 207 | for (int sq = 0; sq < 64; sq++) { 208 | BB_BISHOP[sq] = bb_slide_bishop(sq, 1, 0L); 209 | BB_ROOK[sq] = bb_slide_rook(sq, 1, 0L); 210 | } 211 | 212 | // attack_bishop 213 | int offset = 0; 214 | int squares[64]; 215 | for (int sq = 0; sq < 64; sq++) { 216 | int count = bb_squares(BB_BISHOP[sq], squares); 217 | int n = 1 << count; 218 | for (int i = 0; i < n; i++) { 219 | bb obs = 0; 220 | for (int j = 0; j < count; j++) { 221 | if (i & (1 << j)) { 222 | obs |= BIT(squares[j]); 223 | } 224 | } 225 | bb value = bb_slide_bishop(sq, 0, obs); 226 | int index = (obs * MAGIC_BISHOP[sq]) >> SHIFT_BISHOP[sq]; 227 | bb prev = ATTACK_BISHOP[offset + index]; 228 | if (prev && (prev != value)) { 229 | err("ERROR: invalid ATTACK_BISHOP table"); 230 | } 231 | 232 | ATTACK_BISHOP[offset + index] = value; 233 | } 234 | 235 | OFFSET_BISHOP[sq] = offset; 236 | offset += 1 << (64 - SHIFT_BISHOP[sq]); 237 | } 238 | 239 | // attack_rook 240 | offset = 0; 241 | for (int sq = 0; sq < 64; sq++) { 242 | int count = bb_squares(BB_ROOK[sq], squares); 243 | int n = 1 << count; 244 | for (int i = 0; i < n; i++) { 245 | bb obstacles = 0; 246 | for (int j = 0; j < count; j++) { 247 | if (i & (1 << j)) { 248 | obstacles |= BIT(squares[j]); 249 | } 250 | } 251 | bb value = bb_slide_rook(sq, 0, obstacles); 252 | int index = (obstacles * MAGIC_ROOK[sq]) >> SHIFT_ROOK[sq]; 253 | bb previous = ATTACK_ROOK[offset + index]; 254 | if (previous && previous != value) { 255 | err("ERROR: invalid ATTACK_ROOK table"); 256 | } 257 | ATTACK_ROOK[offset + index] = value; 258 | } 259 | OFFSET_ROOK[sq] = offset; 260 | offset += 1 << (64 - SHIFT_ROOK[sq]); 261 | } 262 | 263 | // BB_KING 264 | const int king_offsets[8][2] = { 265 | {-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}, 266 | }; 267 | for (int rank = 0; rank < 8; rank++) { 268 | for (int file = 0; file < 8; file++) { 269 | bb value = 0; 270 | for (int i = 0; i < 8; i++) { 271 | int r = rank + king_offsets[i][0]; 272 | int f = file + king_offsets[i][1]; 273 | if (r >= 0 && f >= 0 && r < 8 && f < 8) { 274 | value |= BIT(square(r, f)); 275 | } 276 | } 277 | BB_KING[square(rank, file)] = value; 278 | } 279 | } 280 | } 281 | 282 | bb bb_bishop(int sq, bb obs) { 283 | bb value = obs & BB_BISHOP[sq]; 284 | int index = (value * MAGIC_BISHOP[sq]) >> SHIFT_BISHOP[sq]; 285 | return ATTACK_BISHOP[index + OFFSET_BISHOP[sq]]; 286 | } 287 | 288 | bb bb_rook(int sq, bb obs) { 289 | bb value = obs & BB_ROOK[sq]; 290 | int index = (value * MAGIC_ROOK[sq]) >> SHIFT_ROOK[sq]; 291 | return ATTACK_ROOK[index + OFFSET_ROOK[sq]]; 292 | } 293 | 294 | bb bb_queen(int sq, bb obs) { 295 | return bb_bishop(sq, obs) | bb_rook(sq, obs); 296 | } 297 | 298 | void bb_print(bb bbit) { 299 | for (int r = RANK_NB - 1; r >= 0; r--) { 300 | for (int f = 0; f < FILE_NB; f++) { 301 | printf(" %c", (test_bit(bbit, square(r, f))) ? '1' : '.'); 302 | } 303 | printf("\n"); 304 | } 305 | printf("\n"); 306 | } -------------------------------------------------------------------------------- /sisyphus/bb.h: -------------------------------------------------------------------------------- 1 | #ifndef BB_H 2 | #define BB_H 3 | #include "types.h" 4 | #include 5 | #include 6 | 7 | /* Macros */ 8 | #define BIT(sq) (U64(1) << (sq)) 9 | 10 | #define POP_LSB(b, x) \ 11 | b = get_lsb(x); \ 12 | x &= ~BIT(b); 13 | #define POP_MSB(b, x) \ 14 | b = get_msb(x); \ 15 | x &= ~BIT(b); 16 | 17 | #define SET_BIT(bbit, sq) ((bbit) |= BIT(sq)) 18 | #define CLEAR_BIT(bbit, sq) ((bbit) &= ~BIT(sq)) 19 | 20 | // Pre-calculated move bitboards for each piece type and square 21 | extern bb BB_PAWNS[2][64]; 22 | extern bb BB_KNIGHT[64]; 23 | extern bb BB_BISHOP[64]; 24 | extern bb BB_ROOK[64]; 25 | extern bb BB_KING[64]; 26 | 27 | // Initialize bitboard lookup tables 28 | void bb_init(); 29 | 30 | // Returns index of least significant bit 31 | int get_lsb(bb bbit); 32 | 33 | // Returns index of most significant bit 34 | int get_msb(bb bbit); 35 | 36 | // Counts number of set bits in bitboard 37 | int popcount(bb bbit); 38 | 39 | // Checks if bitboard has more than one bit set 40 | int several(bb bbit); 41 | 42 | // Tests if specific square is set in bitboard 43 | bool test_bit(bb bbit, const int sq); 44 | 45 | // Gets file number (0-7) of a square 46 | int file_of(int sq); 47 | 48 | // Gets rank number (0-7) of a square 49 | int rank_of(int sq); 50 | 51 | // Converts rank and file to square index 52 | int square(int rank, int file); 53 | 54 | // Creates piece type combining piece and color 55 | int make_piece_type(int pc, int color); 56 | 57 | // Gets bishop attacks from square considering obstacles 58 | bb bb_bishop(int sq, bb obs); 59 | 60 | // Gets rook attacks from square considering obstacles 61 | bb bb_rook(int sq, bb obs); 62 | 63 | // Gets queen attacks from square considering obstacles 64 | bb bb_queen(int sq, bb obs); 65 | 66 | // Prints visual representation of bitboard 67 | void bb_print(bb bbit); 68 | 69 | #endif // BB_H 70 | -------------------------------------------------------------------------------- /sisyphus/board.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 199309L 2 | 3 | #include "board.h" 4 | #include "time.h" 5 | #include "utils.h" 6 | #include 7 | #include 8 | 9 | #define DURATION 1 10 | #define SIZE (1 << 20) 11 | #define MASK ((SIZE) - 1) 12 | 13 | Entry_t TABLE[SIZE] = {0}; 14 | 15 | static const char *PIECE_SYMBOLS[13] = { 16 | [WHITE_PAWN] = "♟ ", [WHITE_KNIGHT] = "♞ ", [WHITE_BISHOP] = "♝ ", 17 | [WHITE_ROOK] = "♜ ", [WHITE_QUEEN] = "♛ ", [WHITE_KING] = "♚ ", 18 | 19 | [BLACK_PAWN] = "♙ ", [BLACK_KNIGHT] = "♘ ", [BLACK_BISHOP] = "♗ ", 20 | [BLACK_ROOK] = "♖ ", [BLACK_QUEEN] = "♕ ", [BLACK_KING] = "♔ ", 21 | 22 | [NONE] = ". " 23 | }; 24 | 25 | const char *PIECE_LABEL[COLOR_NB] = {"PNBRQK", "pnbrqk"}; 26 | 27 | void *thread_start(void *arg) { 28 | Thread_d *thread_d = (Thread_d *)arg; 29 | 30 | thread_d->score = 31 | best_move(thread_d->search, thread_d->board, thread_d->move, thread_d->debug); 32 | 33 | return NULL; 34 | } 35 | 36 | void thread_stop(Search *search) { 37 | search->stop = true; 38 | } 39 | 40 | void thread_init(Search *search, ChessBoard *board, Move *result, 41 | float duration, 42 | bool debug) { 43 | Thread_d *thread_d = (Thread_d *)calloc(1, sizeof(Thread_d)); 44 | 45 | if (thread_d == NULL) { 46 | err("thread_init(): Could not allocate memory for thread_d"); 47 | } 48 | 49 | thread_d->board = board; 50 | thread_d->search = search; 51 | thread_d->move = result; 52 | thread_d->score = -INF; 53 | thread_d->debug = debug; 54 | 55 | threadpool thpool_p; 56 | thpool_p = thpool_init(1); 57 | 58 | if (thpool_p == NULL) { 59 | free(thread_d); 60 | } 61 | 62 | if (thpool_add_work(thpool_p, (void *)thread_start, (void *)thread_d) == -1) { 63 | free(thread_d); 64 | thpool_destroy(thpool_p); 65 | } 66 | 67 | struct timespec ts; 68 | ts.tv_sec = (time_t)duration; 69 | ts.tv_nsec = (long)((duration - (time_t)duration) * 1000000000L); 70 | nanosleep(&ts, NULL); 71 | 72 | thread_stop(search); 73 | thpool_destroy(thpool_p); 74 | free(thread_d); 75 | } 76 | 77 | void init_table() { 78 | int pc, p, sq; 79 | for (p = PAWN, pc = WHITE_PAWN; p <= KING; pc += 2, p++) { 80 | for (sq = 0; sq < 64; sq++) { 81 | mg_table[pc][sq] = mg_value[p] + mg_pesto_table[p][FLIP(sq)]; 82 | eg_table[pc][sq] = eg_value[p] + eg_pesto_table[p][FLIP(sq)]; 83 | mg_table[pc + 1][sq] = mg_value[p] + mg_pesto_table[p][sq]; 84 | eg_table[pc + 1][sq] = eg_value[p] + eg_pesto_table[p][sq]; 85 | } 86 | } 87 | } 88 | 89 | void board_clear(ChessBoard *board) { 90 | memset(board, 0, sizeof(ChessBoard)); 91 | 92 | for (int i = 0; i < 64; i++) { 93 | board->squares[i] = NONE; 94 | } 95 | 96 | board->castle = CASTLE_ALL; 97 | castling_rights[0] = CASTLE_WHITE_QUEEN_SIDE; 98 | castling_rights[7] = CASTLE_WHITE_KING_SIDE; 99 | castling_rights[56] = CASTLE_BLACK_QUEEN_SIDE; 100 | castling_rights[63] = CASTLE_BLACK_KING_SIDE; 101 | 102 | board->hash = U64(0); 103 | board->pawn_hash = U64(0); 104 | board->mg[WHITE] = board->mg[BLACK] = 0; 105 | board->eg[WHITE] = board->eg[BLACK] = 0; 106 | } 107 | 108 | void board_update(ChessBoard *board, int sq, int piece) { 109 | ASSERT(piece >= WHITE_PAWN && piece <= NONE); 110 | ASSERT(sq >= 0 && sq < SQUARE_NB); 111 | 112 | int prev = board->squares[sq]; 113 | board->squares[sq] = piece; 114 | 115 | if (prev != NONE) { 116 | CLEAR_BIT(board->occ[BOTH], sq); 117 | CLEAR_BIT(board->bb_squares[prev], sq); 118 | if (COLOR(prev)) { 119 | CLEAR_BIT(board->occ[BLACK], sq); 120 | } else { 121 | CLEAR_BIT(board->occ[WHITE], sq); 122 | } 123 | board->hash ^= HASH_PIECES[prev][sq]; 124 | board->mg[COLOR(prev)] -= mg_table[prev][sq]; 125 | board->eg[COLOR(prev)] -= eg_table[prev][sq]; 126 | board->gamePhase -= gamephaseInc[prev]; 127 | } 128 | 129 | if (piece != NONE) { 130 | SET_BIT(board->occ[BOTH], sq); 131 | SET_BIT(board->bb_squares[piece], sq); 132 | if (COLOR(piece)) { 133 | SET_BIT(board->occ[BLACK], sq); 134 | } else { 135 | SET_BIT(board->occ[WHITE], sq); 136 | } 137 | board->hash ^= HASH_PIECES[piece][sq]; 138 | board->mg[COLOR(piece)] += mg_table[piece][sq]; 139 | board->eg[COLOR(piece)] += eg_table[piece][sq]; 140 | board->gamePhase += gamephaseInc[piece]; 141 | } 142 | } 143 | 144 | void board_init(ChessBoard *board) { 145 | if (board == NULL) 146 | return; 147 | 148 | board_clear(board); 149 | init_table(); 150 | init_zobrist(); 151 | 152 | static const int INITIAL_PIECES[COLOR_NB][FILE_NB] = { 153 | { WHITE_ROOK, WHITE_KNIGHT, WHITE_BISHOP, WHITE_QUEEN, WHITE_KING, 154 | WHITE_BISHOP, WHITE_KNIGHT, WHITE_ROOK 155 | }, 156 | { BLACK_ROOK, BLACK_KNIGHT, BLACK_BISHOP, BLACK_QUEEN, BLACK_KING, 157 | BLACK_BISHOP, BLACK_KNIGHT, BLACK_ROOK 158 | } 159 | }; 160 | 161 | for (int file = 0; file < 8; file++) { 162 | board_update(board, square(1, file), WHITE_PAWN); 163 | board_update(board, square(6, file), BLACK_PAWN); 164 | 165 | board_update(board, square(0, file), INITIAL_PIECES[WHITE][file]); 166 | board_update(board, square(7, file), INITIAL_PIECES[BLACK][file]); 167 | } 168 | 169 | board->hash = U64(0); 170 | board->pawn_hash = U64(0); 171 | gen_curr_state_zobrist(board); 172 | gen_pawn_zobrist(board); 173 | } 174 | 175 | void print_board(ChessBoard *board) { 176 | char fen[256]; 177 | 178 | for (int r = RANK_NB - 1; r >= 0; r--) { 179 | printf("%c ", '1' + r); 180 | for (int f = 0; f < FILE_NB; f++) { 181 | printf("%s", PIECE_SYMBOLS[board->squares[square(r, f)]]); 182 | } 183 | printf("\n"); 184 | } 185 | 186 | printf(" a b c d e f g h\n\n"); 187 | 188 | board_to_fen(board, fen); 189 | printf("\n%s\n\n", fen); 190 | } 191 | 192 | int board_drawn_by_repetition(ChessBoard *board, int ply) { 193 | int reps = 0; 194 | 195 | for (int i = board->numMoves - 2; i >= 0; i -= 2) { 196 | if (board->m_history[i] == board->hash && 197 | (i > board->numMoves - ply || ++reps == 2)) 198 | return 1; 199 | } 200 | 201 | return 0; 202 | } 203 | 204 | int board_drawn_by_insufficient_material(ChessBoard *board) { 205 | return !(board->bb_squares[WHITE_PAWN] | board->bb_squares[BLACK_PAWN] | 206 | board->bb_squares[WHITE_ROOK] | board->bb_squares[BLACK_ROOK] | 207 | board->bb_squares[WHITE_QUEEN] | board->bb_squares[BLACK_QUEEN]) && 208 | (!several(board->occ[WHITE]) || !several(board->occ[BLACK])) && 209 | (!several(board->bb_squares[WHITE_KNIGHT] | 210 | board->bb_squares[BLACK_KNIGHT] | 211 | board->bb_squares[WHITE_BISHOP] | 212 | board->bb_squares[BLACK_BISHOP]) || 213 | (!(board->bb_squares[WHITE_BISHOP] | 214 | board->bb_squares[BLACK_BISHOP]) && 215 | popcount(board->bb_squares[WHITE_KNIGHT] | 216 | board->bb_squares[BLACK_KNIGHT]) <= 2)); 217 | } 218 | 219 | int is_draw(ChessBoard *board, int ply) { 220 | return board_drawn_by_insufficient_material(board) || 221 | board_drawn_by_repetition(board, ply); 222 | } 223 | 224 | int string_to_sq(const char *str) { 225 | return str[0] == '-' ? -1 : square(str[1] - '1', str[0] - 'a'); 226 | } 227 | 228 | void sq_to_string(int sq, char *str) { 229 | assert(-1 <= sq && sq < SQUARE_NB); 230 | 231 | if (sq == -1) 232 | *str++ = '-'; 233 | else { 234 | *str++ = file_of(sq) + 'a'; 235 | *str++ = rank_of(sq) + '1'; 236 | } 237 | 238 | *str++ = '\0'; 239 | } 240 | 241 | void board_load_fen(ChessBoard *board, const char *fen) { 242 | board_clear(board); 243 | 244 | int rank = 7, file = 0; 245 | char ch; 246 | char *str = strdup(fen), *save_p = NULL; 247 | if (str == NULL) 248 | return; 249 | char *token = strtok_r(str, " ", &save_p); 250 | 251 | while ((ch = *token++)) { 252 | if (isdigit(ch)) 253 | file += ch - '0'; 254 | else if (ch == '/') { 255 | file = 0; 256 | rank--; 257 | } else { 258 | int piece = NONE; 259 | switch (ch) { 260 | case 'P': 261 | piece = WHITE_PAWN; 262 | break; 263 | case 'N': 264 | piece = WHITE_KNIGHT; 265 | break; 266 | case 'B': 267 | piece = WHITE_BISHOP; 268 | break; 269 | case 'R': 270 | piece = WHITE_ROOK; 271 | break; 272 | case 'Q': 273 | piece = WHITE_QUEEN; 274 | break; 275 | case 'K': 276 | piece = WHITE_KING; 277 | break; 278 | case 'p': 279 | piece = BLACK_PAWN; 280 | break; 281 | case 'n': 282 | piece = BLACK_KNIGHT; 283 | break; 284 | case 'b': 285 | piece = BLACK_BISHOP; 286 | break; 287 | case 'r': 288 | piece = BLACK_ROOK; 289 | break; 290 | case 'q': 291 | piece = BLACK_QUEEN; 292 | break; 293 | case 'k': 294 | piece = BLACK_KING; 295 | break; 296 | } 297 | if (piece != NONE) 298 | board_update(board, square(rank, file++), piece); 299 | } 300 | } 301 | 302 | token = strtok_r(NULL, " ", &save_p); 303 | board->color = token[0] == 'w' ? WHITE : BLACK; 304 | 305 | board->castle = 0; 306 | token = strtok_r(NULL, " ", &save_p); 307 | bool done = false; 308 | while ((!done) && (ch = *token++)) { 309 | switch (ch) { 310 | case 'K': 311 | board->castle |= CASTLE_WHITE_KING_SIDE; 312 | break; 313 | case 'Q': 314 | board->castle |= CASTLE_WHITE_QUEEN_SIDE; 315 | break; 316 | case 'k': 317 | board->castle |= CASTLE_BLACK_KING_SIDE; 318 | break; 319 | case 'q': 320 | board->castle |= CASTLE_BLACK_QUEEN_SIDE; 321 | break; 322 | case '-': 323 | done = true; 324 | break; 325 | case ' ': 326 | done = true; 327 | break; 328 | } 329 | } 330 | 331 | token = strtok_r(NULL, " ", &save_p); 332 | if (token != NULL) { 333 | int sq = string_to_sq(token); 334 | if (sq != -1) 335 | SET_BIT(board->ep, sq); 336 | } 337 | 338 | board->numMoves = 0; 339 | board->hash = U64(0); 340 | board->pawn_hash = U64(0); 341 | gen_curr_state_zobrist(board); 342 | gen_pawn_zobrist(board); 343 | 344 | free(str); 345 | } 346 | 347 | void board_to_fen(ChessBoard *board, char *fen) { 348 | char str[3]; 349 | int sq; 350 | 351 | for (int r = RANK_NB - 1; r >= 0; r--) { 352 | int cnt = 0; 353 | for (int f = 0; f < FILE_NB; f++) { 354 | const int p = board->squares[square(r, f)]; 355 | 356 | if (p != NONE) { 357 | if (cnt) 358 | *fen++ = cnt + '0'; 359 | *fen++ = PIECE_LABEL[COLOR(p)][PIECE(p)]; 360 | cnt = 0; 361 | } else 362 | cnt++; 363 | } 364 | if (cnt) 365 | *fen++ = cnt + '0'; 366 | 367 | *fen++ = r == 0 ? ' ' : '/'; 368 | } 369 | 370 | *fen++ = board->color == WHITE ? 'w' : 'b'; 371 | *fen++ = ' '; 372 | 373 | if (board->castle) { 374 | if (board->castle & CASTLE_WHITE_KING_SIDE) 375 | *fen++ = 'K'; 376 | if (board->castle & CASTLE_WHITE_QUEEN_SIDE) 377 | *fen++ = 'Q'; 378 | if (board->castle & CASTLE_BLACK_KING_SIDE) 379 | *fen++ = 'k'; 380 | if (board->castle & CASTLE_BLACK_QUEEN_SIDE) 381 | *fen++ = 'q'; 382 | 383 | } else 384 | *fen++ = '-'; 385 | 386 | sq = !!board->ep ? get_lsb(board->ep) : -1; 387 | sq_to_string(sq, str); 388 | 389 | sprintf(fen, " %s", str); 390 | } 391 | 392 | bb perft_test(ChessBoard *board, int depth) { 393 | Undo undo; 394 | Move moves[MAX_MOVES]; 395 | bb nodes = U64(0); 396 | int count = 0; 397 | depth = MAX(depth, 0); 398 | 399 | if (!depth) 400 | return U64(1); 401 | 402 | Entry_t *entry = &TABLE[(board->hash & MASK)]; 403 | 404 | if (entry->key == board->hash && entry->depth == depth) { 405 | return entry->value; 406 | } 407 | count = gen_moves(board, moves); 408 | for (count -= 1; count >= 0; count--) { 409 | Move move = moves[count]; 410 | do_move(board, move, &undo); 411 | if (!illegal_to_move(board)) 412 | nodes += perft_test(board, depth - 1); 413 | undo_move(board, move, &undo); 414 | } 415 | 416 | entry->depth = depth; 417 | entry->key = board->hash; 418 | entry->value = nodes; 419 | 420 | return nodes; 421 | } 422 | 423 | const int pawn_square_values[64] = { 424 | 0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 10, -20, -20, 10, 10, 5, 425 | 5, -5, -10, 0, 0, -10, -5, 5, 0, 0, 0, 20, 20, 0, 0, 0, 426 | 5, 5, 10, 25, 25, 10, 5, 5, 10, 10, 20, 30, 30, 20, 10, 10, 427 | 50, 50, 50, 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, 0, 0, 0, 428 | }; 429 | 430 | const int knight_square_values[64] = { 431 | -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 5, 5, 432 | 0, -20, -40, -30, 5, 10, 15, 15, 10, 5, -30, -30, 0, 433 | 15, 20, 20, 15, 0, -30, -30, 5, 15, 20, 20, 15, 5, 434 | -30, -30, 0, 10, 15, 15, 10, 0, -30, -40, -20, 0, 0, 435 | 0, 0, -20, -40, -50, -40, -30, -30, -30, -30, -40, -50, 436 | }; 437 | 438 | const int bishop_square_values[64] = { 439 | -20, -10, -10, -10, -10, -10, -10, -20, -10, 5, 0, 0, 0, 440 | 0, 5, -10, -10, 10, 10, 10, 10, 10, 10, -10, -10, 0, 441 | 10, 10, 10, 10, 0, -10, -10, 5, 5, 10, 10, 5, 5, 442 | -10, -10, 0, 5, 10, 10, 5, 0, -10, -10, 0, 0, 0, 443 | 0, 0, 0, -10, -20, -10, -10, -10, -10, -10, -10, -20, 444 | }; 445 | 446 | const int rook_square_values[64] = { 447 | 0, 0, 0, 5, 5, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -5, 448 | -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, 449 | -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, 450 | 5, 10, 10, 10, 10, 10, 10, 5, 0, 0, 0, 0, 0, 0, 0, 0, 451 | }; 452 | 453 | const int king_square_values[64] = { 454 | 20, 30, 10, 0, 0, 10, 30, 20, 20, 20, 0, 0, 0, 455 | 0, 20, 20, -10, -20, -20, -20, -20, -20, -20, -10, -20, -30, 456 | -30, -40, -40, -30, -30, -20, -30, -40, -40, -50, -50, -40, -40, 457 | -30, -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, 458 | -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, 459 | }; 460 | 461 | const int queen_square_values[64] = { 462 | -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 5, 0, 0, 0, 0, -10, 463 | -10, 5, 5, 5, 5, 5, 0, -10, 0, 0, 5, 5, 5, 5, 0, -5, 464 | -5, 0, 5, 5, 5, 5, 0, -5, -10, 0, 5, 5, 5, 5, 0, -10, 465 | -10, 0, 0, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20, 466 | }; 467 | 468 | int mg_value[6] = {82, 337, 365, 477, 1025, 0}; 469 | int eg_value[6] = {94, 281, 297, 512, 936, 0}; 470 | 471 | int mg_pawn_table[64] = { 472 | 0, 0, 0, 0, 0, 0, 0, 0, 98, 134, 61, 95, 68, 126, 34, -11, 473 | -6, 7, 26, 31, 65, 56, 25, -20, -14, 13, 6, 21, 23, 12, 17, -23, 474 | -27, -2, -5, 12, 17, 6, 10, -25, -26, -4, -4, -10, 3, 3, 33, -12, 475 | -35, -1, -20, -23, -15, 24, 38, -22, 0, 0, 0, 0, 0, 0, 0, 0, 476 | }; 477 | 478 | int eg_pawn_table[64] = { 479 | 0, 0, 0, 0, 0, 0, 0, 0, 178, 173, 158, 134, 147, 132, 165, 187, 480 | 94, 100, 85, 67, 56, 53, 82, 84, 32, 24, 13, 5, -2, 4, 17, 17, 481 | 13, 9, -3, -7, -7, -8, 3, -1, 4, 7, -6, 1, 0, -5, -1, -8, 482 | 13, 8, 8, 10, 13, 0, 2, -7, 0, 0, 0, 0, 0, 0, 0, 0, 483 | }; 484 | 485 | int mg_knight_table[64] = { 486 | -167, -89, -34, -49, 61, -97, -15, -107, -73, -41, 72, 36, 23, 487 | 62, 7, -17, -47, 60, 37, 65, 84, 129, 73, 44, -9, 17, 488 | 19, 53, 37, 69, 18, 22, -13, 4, 16, 13, 28, 19, 21, 489 | -8, -23, -9, 12, 10, 19, 17, 25, -16, -29, -53, -12, -3, 490 | -1, 18, -14, -19, -105, -21, -58, -33, -17, -28, -19, -23, 491 | }; 492 | 493 | int eg_knight_table[64] = { 494 | -58, -38, -13, -28, -31, -27, -63, -99, -25, -8, -25, -2, -9, 495 | -25, -24, -52, -24, -20, 10, 9, -1, -9, -19, -41, -17, 3, 496 | 22, 22, 22, 11, 8, -18, -18, -6, 16, 25, 16, 17, 4, 497 | -18, -23, -3, -1, 15, 10, -3, -20, -22, -42, -20, -10, -5, 498 | -2, -20, -23, -44, -29, -51, -23, -15, -22, -18, -50, -64, 499 | }; 500 | 501 | int mg_bishop_table[64] = { 502 | -29, 4, -82, -37, -25, -42, 7, -8, -26, 16, -18, -13, 30, 59, 18, -47, 503 | -16, 37, 43, 40, 35, 50, 37, -2, -4, 5, 19, 50, 37, 37, 7, -2, 504 | -6, 13, 13, 26, 34, 12, 10, 4, 0, 15, 15, 15, 14, 27, 18, 10, 505 | 4, 15, 16, 0, 7, 21, 33, 1, -33, -3, -14, -21, -13, -12, -39, -21, 506 | }; 507 | 508 | int eg_bishop_table[64] = { 509 | -14, -21, -11, -8, -7, -9, -17, -24, -8, -4, 7, -12, -3, -13, -4, -14, 510 | 2, -8, 0, -1, -2, 6, 0, 4, -3, 9, 12, 9, 14, 10, 3, 2, 511 | -6, 3, 13, 19, 7, 10, -3, -9, -12, -3, 8, 10, 13, 3, -7, -15, 512 | -14, -18, -7, -1, 4, -9, -15, -27, -23, -9, -23, -5, -9, -16, -5, -17, 513 | }; 514 | 515 | int mg_rook_table[64] = { 516 | 32, 42, 32, 51, 63, 9, 31, 43, 27, 32, 58, 62, 80, 67, 26, 44, 517 | -5, 19, 26, 36, 17, 45, 61, 16, -24, -11, 7, 26, 24, 35, -8, -20, 518 | -36, -26, -12, -1, 9, -7, 6, -23, -45, -25, -16, -17, 3, 0, -5, -33, 519 | -44, -16, -20, -9, -1, 11, -6, -71, -19, -13, 1, 17, 16, 7, -37, -26, 520 | }; 521 | 522 | int eg_rook_table[64] = { 523 | 13, 10, 18, 15, 12, 12, 8, 5, 11, 13, 13, 11, -3, 3, 8, 3, 524 | 7, 7, 7, 5, 4, -3, -5, -3, 4, 3, 13, 1, 2, 1, -1, 2, 525 | 3, 5, 8, 4, -5, -6, -8, -11, -4, 0, -5, -1, -7, -12, -8, -16, 526 | -6, -6, 0, 2, -9, -9, -11, -3, -9, 2, 3, -1, -5, -13, 4, -20, 527 | }; 528 | 529 | int mg_queen_table[64] = { 530 | -28, 0, 29, 12, 59, 44, 43, 45, -24, -39, -5, 1, -16, 57, 28, 54, 531 | -13, -17, 7, 8, 29, 56, 47, 57, -27, -27, -16, -16, -1, 17, -2, 1, 532 | -9, -26, -9, -10, -2, -4, 3, -3, -14, 2, -11, -2, -5, 2, 14, 5, 533 | -35, -8, 11, 2, 8, 15, -3, 1, -1, -18, -9, 10, -15, -25, -31, -50, 534 | }; 535 | 536 | int eg_queen_table[64] = { 537 | -9, 22, 22, 27, 27, 19, 10, 20, -17, 20, 32, 41, 58, 538 | 25, 30, 0, -20, 6, 9, 49, 47, 35, 19, 9, 3, 22, 539 | 24, 45, 57, 40, 57, 36, -18, 28, 19, 47, 31, 34, 39, 540 | 23, -16, -27, 15, 6, 9, 17, 10, 5, -22, -23, -30, -16, 541 | -16, -23, -36, -32, -33, -28, -22, -43, -5, -32, -20, -41, 542 | }; 543 | 544 | int mg_king_table[64] = { 545 | -65, 23, 16, -15, -56, -34, 2, 13, 29, -1, -20, -7, -8, 546 | -4, -38, -29, -9, 24, 2, -16, -20, 6, 22, -22, -17, -20, 547 | -12, -27, -30, -25, -14, -36, -49, -1, -27, -39, -46, -44, -33, 548 | -51, -14, -14, -22, -46, -44, -30, -15, -27, 1, 7, -8, -64, 549 | -43, -16, 9, 8, -15, 36, 12, -54, 8, -28, 24, 14, 550 | }; 551 | 552 | int eg_king_table[64] = {-74, -35, -18, -18, -11, 15, 4, -17, -12, 17, 14, 553 | 17, 17, 38, 23, 11, 10, 17, 23, 15, 20, 45, 554 | 44, 13, -8, 22, 24, 27, 26, 33, 26, 3, -18, 555 | -4, 21, 24, 27, 23, 9, -11, -19, -3, 11, 21, 556 | 23, 16, 7, -9, -27, -11, 4, 13, 14, 4, -5, 557 | -17, -53, -34, -21, -11, -28, -14, -24, -43 558 | }; 559 | 560 | int piece_material[13] = {[WHITE_PAWN... BLACK_PAWN] = PAWN_MATERIAL, 561 | [WHITE_KNIGHT... BLACK_KNIGHT] = KNIGHT_MATERIAL, 562 | [WHITE_BISHOP... BLACK_BISHOP] = BISHOP_MATERIAL, 563 | [WHITE_ROOK... BLACK_ROOK] = ROOK_MATERIAL, 564 | [WHITE_QUEEN... BLACK_QUEEN] = QUEEN_MATERIAL, 565 | [WHITE_KING... BLACK_KING] = KING_MATERIAL 566 | }; 567 | 568 | const int *square_values[13] = { 569 | [WHITE_PAWN... BLACK_PAWN] = pawn_square_values, 570 | [WHITE_KNIGHT... BLACK_KNIGHT] = knight_square_values, 571 | [WHITE_BISHOP... BLACK_BISHOP] = bishop_square_values, 572 | [WHITE_ROOK... BLACK_ROOK] = rook_square_values, 573 | [WHITE_QUEEN... BLACK_QUEEN] = queen_square_values, 574 | [WHITE_KING... BLACK_KING] = king_square_values 575 | 576 | }; 577 | 578 | int castling_rights[64] = {0}; 579 | 580 | int *mg_pesto_table[6] = {mg_pawn_table, mg_knight_table, mg_bishop_table, 581 | mg_rook_table, mg_queen_table, mg_king_table 582 | }; 583 | 584 | int *eg_pesto_table[6] = {eg_pawn_table, eg_knight_table, eg_bishop_table, 585 | eg_rook_table, eg_queen_table, eg_king_table 586 | }; 587 | 588 | int gamephaseInc[12] = {0, 0, 1, 1, 1, 1, 2, 2, 4, 4, 0, 0}; 589 | int mg_table[12][64]; 590 | int eg_table[12][64]; -------------------------------------------------------------------------------- /sisyphus/board.h: -------------------------------------------------------------------------------- 1 | #ifndef BOARD_H 2 | #define BOARD_H 3 | 4 | #include "bb.h" 5 | #include "search.h" 6 | #include "thpool.h" 7 | #include "types.h" 8 | #include "zobrist.h" 9 | #include 10 | #include 11 | 12 | // Piece square tables for positional evaluation 13 | extern const int pawn_square_values[64]; 14 | extern const int knight_square_values[64]; 15 | extern const int bishop_square_values[64]; 16 | extern const int rook_square_values[64]; 17 | extern const int king_square_values[64]; 18 | extern const int queen_square_values[64]; 19 | 20 | // Material values for middle and endgame phases 21 | extern int mg_value[6]; 22 | extern int eg_value[6]; 23 | 24 | // Piece square tables for middle and endgame phases 25 | extern int mg_pawn_table[64]; 26 | extern int eg_pawn_table[64]; 27 | extern int mg_knight_table[64]; 28 | extern int eg_knight_table[64]; 29 | extern int mg_bishop_table[64]; 30 | extern int eg_bishop_table[64]; 31 | extern int mg_rook_table[64]; 32 | extern int eg_rook_table[64]; 33 | extern int mg_queen_table[64]; 34 | extern int eg_queen_table[64]; 35 | extern int mg_king_table[64]; 36 | extern int eg_king_table[64]; 37 | 38 | // PESTO evaluation tables 39 | extern int *mg_pesto_table[6]; 40 | extern int *eg_pesto_table[6]; 41 | extern int mg_table[12][64]; 42 | extern int eg_table[12][64]; 43 | 44 | // Game phase weights for each piece 45 | extern int gamephaseInc[12]; 46 | 47 | // Lookup tables for piece values and castling 48 | extern const int *square_values[13]; 49 | extern int piece_material[13]; 50 | extern int castling_rights[64]; 51 | 52 | /* Macros */ 53 | #define MAX(x, y) (x ^ ((x ^ y) & -(x < y))) // Get maximum of two values 54 | #define MIN(x, y) (y ^ ((x ^ y) & -(x < y))) // Get minimum of two values 55 | #define FLIP(sq) ((sq ^ 56)) // Flip square vertically 56 | #define FLIP_63(sq) ((sq ^ 63)) // Flip square diagonally 57 | #define PIECE(x) (((x) & ~1) >> 1) // Get piece type from piece code 58 | #define COLOR(x) ((x) & 1) // Get piece color 59 | #define SWITCH_SIDE(x) (x->color ^= BLACK) // Switch side to move 60 | 61 | // Board manipulation functions 62 | void board_init(ChessBoard *b); // Initialize chess board 63 | void print_board(ChessBoard *b); // Print board representation 64 | void board_load_fen(ChessBoard *board, const char *fen); // Load position from FEN 65 | void board_to_fen(ChessBoard *board, char *fen); // Convert position to FEN 66 | void board_update(ChessBoard *board, int sq, int piece); // Update board state 67 | 68 | // Game state evaluation 69 | int board_drawn_by_insufficient_material(ChessBoard *board); // Check material draw 70 | int is_draw(ChessBoard *board, int ply); // Check if position is drawn 71 | 72 | // Testing and threading 73 | bb perft_test(ChessBoard *board, int depth); // Performance test 74 | void thread_init(Search *search, ChessBoard *board, Move *result, float duration, bool debug); // Initialize search thread 75 | void thread_stop(Search *search); // Stop search thread 76 | 77 | #endif // BOARD_H -------------------------------------------------------------------------------- /sisyphus/eval.c: -------------------------------------------------------------------------------- 1 | #include "eval.h" 2 | 3 | int eval(ChessBoard *board) { 4 | return pesto_eval(board); 5 | } 6 | 7 | int pesto_eval(ChessBoard *board) { 8 | int side2move = board->color; 9 | int mgScore = board->mg[side2move] - board->mg[side2move ^ BLACK]; 10 | int egScore = board->eg[side2move] - board->eg[side2move ^ BLACK]; 11 | int mgPhase = board->gamePhase; 12 | if (mgPhase > 24) 13 | mgPhase = 24; 14 | int egPhase = 24 - mgPhase; 15 | return (mgScore * mgPhase + egScore * egPhase) / 24; 16 | } 17 | 18 | int count_stacked_pawns(bb pawns, int count) { 19 | int result = 0; 20 | result += popcount(pawns & FILE_A) == count; 21 | result += popcount(pawns & FILE_B) == count; 22 | result += popcount(pawns & FILE_C) == count; 23 | result += popcount(pawns & FILE_D) == count; 24 | result += popcount(pawns & FILE_E) == count; 25 | result += popcount(pawns & FILE_F) == count; 26 | result += popcount(pawns & FILE_G) == count; 27 | result += popcount(pawns & FILE_H) == count; 28 | return result; 29 | } 30 | 31 | int evaluate_white_pawns(ChessBoard *board) { 32 | bb pawns = board->bb_squares[WHITE_PAWN]; 33 | int score = 0; 34 | score -= count_stacked_pawns(pawns, 2) * 50; 35 | score -= count_stacked_pawns(pawns, 3) * 100; 36 | return score; 37 | } 38 | 39 | int evaluate_black_pawns(ChessBoard *board) { 40 | bb pawns = board->bb_squares[BLACK_PAWN]; 41 | int score = 0; 42 | score -= count_stacked_pawns(pawns, 2) * 50; 43 | score -= count_stacked_pawns(pawns, 3) * 100; 44 | return score; 45 | } 46 | 47 | int evaluate_pawns(ChessBoard *board) { 48 | int score = 0; 49 | score += evaluate_white_pawns(board); 50 | score -= evaluate_black_pawns(board); 51 | 52 | return (board->color == WHITE) ? score : -score; 53 | } -------------------------------------------------------------------------------- /sisyphus/eval.h: -------------------------------------------------------------------------------- 1 | #ifndef EVAL_H 2 | #define EVAL_H 3 | 4 | #include "bb.h" 5 | #include "board.h" 6 | #include "types.h" 7 | 8 | int pesto_eval(ChessBoard *board); 9 | 10 | int evaluate_pawns(ChessBoard *board); 11 | 12 | int eval(ChessBoard *board); 13 | 14 | #endif // EVAL_H -------------------------------------------------------------------------------- /sisyphus/gen.c: -------------------------------------------------------------------------------- 1 | #include "gen.h" 2 | 3 | int gen_knight_moves(Move *moves, bb srcs, bb mask, int color) { 4 | Move *ptr = moves; 5 | int src, dst; 6 | while (srcs) { 7 | POP_LSB(src, srcs); 8 | bb dsts = BB_KNIGHT[src] & mask; 9 | while (dsts) { 10 | POP_LSB(dst, dsts); 11 | EMIT_MOVE(moves, src, dst, make_piece_type(KNIGHT, color), EMPTY_FLAG); 12 | } 13 | } 14 | 15 | return moves - ptr; 16 | } 17 | 18 | int gen_bishop_moves(Move *moves, bb srcs, bb mask, bb all, int color) { 19 | Move *ptr = moves; 20 | int src, dst; 21 | while (srcs) { 22 | POP_LSB(src, srcs); 23 | bb dsts = bb_bishop(src, all) & mask; 24 | while (dsts) { 25 | POP_LSB(dst, dsts); 26 | EMIT_MOVE(moves, src, dst, make_piece_type(BISHOP, color), EMPTY_FLAG); 27 | } 28 | } 29 | return moves - ptr; 30 | } 31 | 32 | int gen_rook_moves(Move *moves, bb srcs, bb mask, bb all, int color) { 33 | Move *ptr = moves; 34 | int src, dst; 35 | while (srcs) { 36 | POP_LSB(src, srcs); 37 | bb dsts = bb_rook(src, all) & mask; 38 | while (dsts) { 39 | POP_LSB(dst, dsts); 40 | EMIT_MOVE(moves, src, dst, make_piece_type(ROOK, color), EMPTY_FLAG); 41 | } 42 | } 43 | return moves - ptr; 44 | } 45 | 46 | int gen_queen_moves(Move *moves, bb srcs, bb mask, bb all, int color) { 47 | Move *ptr = moves; 48 | int src, dst; 49 | while (srcs) { 50 | POP_LSB(src, srcs); 51 | bb dsts = bb_queen(src, all) & mask; 52 | while (dsts) { 53 | POP_LSB(dst, dsts); 54 | EMIT_MOVE(moves, src, dst, make_piece_type(QUEEN, color), EMPTY_FLAG); 55 | } 56 | } 57 | return moves - ptr; 58 | } 59 | 60 | int gen_king_moves(Move *moves, bb srcs, bb mask, int color) { 61 | Move *ptr = moves; 62 | int src, dst; 63 | while (srcs) { 64 | POP_LSB(src, srcs); 65 | bb dsts = BB_KING[src] & mask; 66 | while (dsts) { 67 | POP_LSB(dst, dsts); 68 | EMIT_MOVE(moves, src, dst, make_piece_type(KING, color), EMPTY_FLAG); 69 | } 70 | } 71 | return moves - ptr; 72 | } 73 | 74 | int gen_white_pawn_moves(ChessBoard *board, Move *moves) { 75 | Move *ptr = moves; 76 | bb pawns = board->bb_squares[WHITE_PAWN]; 77 | bb mask = board->occ[BLACK] | board->ep; 78 | bb promo = 0xff00000000000000L; 79 | bb p1 = (pawns << 8) & ~board->occ[BOTH]; 80 | bb p2 = ((p1 & 0x0000000000ff0000L) << 8) & ~board->occ[BOTH]; 81 | bb a1 = ((pawns & 0xfefefefefefefefeL) << 7) & mask; 82 | bb a2 = ((pawns & 0x7f7f7f7f7f7f7f7fL) << 9) & mask; 83 | int sq; 84 | 85 | while (p1) { 86 | POP_LSB(sq, p1); 87 | if (test_bit(promo, sq)) { 88 | EMIT_PROMOTIONS(moves, sq - 8, sq, WHITE_PAWN); 89 | } else { 90 | EMIT_MOVE(moves, sq - 8, sq, WHITE_PAWN, EMPTY_FLAG); 91 | } 92 | } 93 | 94 | while (p2) { 95 | POP_LSB(sq, p2); 96 | EMIT_MOVE(moves, sq - 16, sq, WHITE_PAWN, EMPTY_FLAG); 97 | } 98 | 99 | while (a1) { 100 | POP_LSB(sq, a1); 101 | if (test_bit(promo, sq)) { 102 | EMIT_PROMOTIONS(moves, sq - 7, sq, WHITE_PAWN); 103 | } else { 104 | if (test_bit(board->ep, sq)) { 105 | EMIT_EN_PASSANT(moves, sq - 7, sq, WHITE_PAWN); 106 | } else { 107 | EMIT_MOVE(moves, sq - 7, sq, WHITE_PAWN, EMPTY_FLAG); 108 | } 109 | } 110 | } 111 | 112 | while (a2) { 113 | POP_LSB(sq, a2); 114 | if (test_bit(promo, sq)) { 115 | EMIT_PROMOTIONS(moves, sq - 9, sq, WHITE_PAWN); 116 | } else { 117 | if (test_bit(board->ep, sq)) { 118 | EMIT_EN_PASSANT(moves, sq - 9, sq, WHITE_PAWN); 119 | } else { 120 | EMIT_MOVE(moves, sq - 9, sq, WHITE_PAWN, EMPTY_FLAG); 121 | } 122 | } 123 | } 124 | 125 | return moves - ptr; 126 | } 127 | 128 | INLINE int gen_white_knight_moves(ChessBoard *board, Move *moves) { 129 | return gen_knight_moves(moves, board->bb_squares[WHITE_KNIGHT], 130 | ~board->occ[WHITE], WHITE); 131 | } 132 | 133 | INLINE int gen_white_bishop_moves(ChessBoard *board, Move *moves) { 134 | return gen_bishop_moves(moves, board->bb_squares[WHITE_BISHOP], 135 | ~board->occ[WHITE], board->occ[BOTH], WHITE); 136 | } 137 | 138 | INLINE int gen_white_rook_moves(ChessBoard *board, Move *moves) { 139 | return gen_rook_moves(moves, board->bb_squares[WHITE_ROOK], 140 | ~board->occ[WHITE], board->occ[BOTH], WHITE); 141 | } 142 | 143 | INLINE int gen_white_queen_moves(ChessBoard *board, Move *moves) { 144 | return gen_queen_moves(moves, board->bb_squares[WHITE_QUEEN], 145 | ~board->occ[WHITE], board->occ[BOTH], WHITE); 146 | } 147 | 148 | INLINE int gen_white_king_moves(ChessBoard *board, Move *moves) { 149 | return gen_king_moves(moves, board->bb_squares[WHITE_KING], 150 | ~board->occ[WHITE], WHITE); 151 | } 152 | 153 | int gen_white_king_castle(ChessBoard *board, Move *moves) { 154 | Move *ptr = moves; 155 | if (board->castle & CASTLE_WHITE_KING_SIDE) { 156 | if (!(board->occ[BOTH] & 0x0000000000000060L)) { 157 | Move dummy[MAX_MOVES]; 158 | bb mask = 0x0000000000000030L; 159 | if (!(gen_black_attacks_against(board, dummy, mask))) { 160 | EMIT_CASTLE(moves, 4, 6, WHITE_KING); 161 | } 162 | } 163 | } 164 | if (board->castle & CASTLE_WHITE_QUEEN_SIDE) { 165 | if (!(board->occ[BOTH] & 0x000000000000000eL)) { 166 | Move dummy[MAX_MOVES]; 167 | bb mask = 0x0000000000000018L; 168 | if (!(gen_black_attacks_against(board, dummy, mask))) { 169 | EMIT_CASTLE(moves, 4, 2, WHITE_KING); 170 | } 171 | } 172 | } 173 | return moves - ptr; 174 | } 175 | 176 | INLINE int gen_white_moves(ChessBoard *board, Move *moves) { 177 | static const MoveGen white_generators[] = { 178 | gen_white_pawn_moves, gen_white_knight_moves, gen_white_bishop_moves, 179 | gen_white_rook_moves, gen_white_queen_moves, gen_white_king_moves, 180 | gen_white_king_castle 181 | }; 182 | 183 | Move *ptr = moves; 184 | for (size_t i = 0; i < sizeof(white_generators) / sizeof(white_generators[0]); 185 | i++) { 186 | moves += white_generators[i](board, moves); 187 | } 188 | 189 | return moves - ptr; 190 | } 191 | 192 | int gen_white_pawn_attacks_against(ChessBoard *board, Move *moves, bb mask) { 193 | Move *ptr = moves; 194 | bb pawns = board->bb_squares[WHITE_PAWN]; 195 | bb a1 = ((pawns & 0xfefefefefefefefeL) << 7) & mask; 196 | bb a2 = ((pawns & 0x7f7f7f7f7f7f7f7fL) << 9) & mask; 197 | int sq; 198 | 199 | while (a1) { 200 | POP_LSB(sq, a1); 201 | EMIT_MOVE(moves, sq - 7, sq, WHITE_PAWN, EMPTY_FLAG); 202 | } 203 | 204 | while (a2) { 205 | POP_LSB(sq, a2); 206 | EMIT_MOVE(moves, sq - 9, sq, WHITE_PAWN, EMPTY_FLAG); 207 | } 208 | 209 | return moves - ptr; 210 | } 211 | 212 | INLINE int gen_white_knight_attacks_against(ChessBoard *board, Move *moves, 213 | bb mask) { 214 | return gen_knight_moves(moves, board->bb_squares[WHITE_KNIGHT], mask, WHITE); 215 | } 216 | 217 | INLINE int gen_white_bishop_attacks_against(ChessBoard *board, Move *moves, 218 | bb mask) { 219 | return gen_bishop_moves(moves, board->bb_squares[WHITE_BISHOP], mask, 220 | board->occ[BOTH], WHITE); 221 | } 222 | 223 | INLINE int gen_white_rook_attacks_against(ChessBoard *board, Move *moves, 224 | bb mask) { 225 | return gen_rook_moves(moves, board->bb_squares[WHITE_ROOK], mask, 226 | board->occ[BOTH], WHITE); 227 | } 228 | 229 | INLINE int gen_white_queen_attacks_against(ChessBoard *board, Move *moves, 230 | bb mask) { 231 | return gen_queen_moves(moves, board->bb_squares[WHITE_QUEEN], mask, 232 | board->occ[BOTH], WHITE); 233 | } 234 | 235 | INLINE int gen_white_king_attacks_against(ChessBoard *board, Move *moves, 236 | bb mask) { 237 | return gen_king_moves(moves, board->bb_squares[WHITE_KING], mask, WHITE); 238 | } 239 | 240 | INLINE int gen_white_attacks_against(ChessBoard *board, Move *moves, bb mask) { 241 | static const AttacksGen white_attack_generators[] = { 242 | gen_white_pawn_attacks_against, gen_white_knight_attacks_against, 243 | gen_white_bishop_attacks_against, gen_white_rook_attacks_against, 244 | gen_white_queen_attacks_against, gen_white_king_attacks_against 245 | }; 246 | 247 | Move *ptr = moves; 248 | for (size_t i = 0; 249 | i < sizeof(white_attack_generators) / sizeof(white_attack_generators[0]); 250 | i++) { 251 | moves += white_attack_generators[i](board, moves, mask); 252 | } 253 | 254 | return moves - ptr; 255 | } 256 | 257 | INLINE int gen_white_checks(ChessBoard *board, Move *moves) { 258 | return gen_white_attacks_against(board, moves, board->bb_squares[BLACK_KING]); 259 | } 260 | 261 | int gen_black_pawn_moves(ChessBoard *board, Move *moves) { 262 | Move *ptr = moves; 263 | bb pawns = board->bb_squares[BLACK_PAWN]; 264 | bb mask = board->occ[WHITE] | board->ep; 265 | 266 | bb promo = 0x00000000000000ffL; 267 | bb p1 = (pawns >> 8) & ~board->occ[BOTH]; 268 | bb p2 = ((p1 & 0x0000ff0000000000L) >> 8) & ~board->occ[BOTH]; 269 | bb a1 = ((pawns & 0x7f7f7f7f7f7f7f7fL) >> 7) & mask; 270 | bb a2 = ((pawns & 0xfefefefefefefefeL) >> 9) & mask; 271 | int sq; 272 | while (p1) { 273 | POP_LSB(sq, p1); 274 | if (test_bit(promo, sq)) { 275 | EMIT_PROMOTIONS(moves, sq + 8, sq, BLACK_PAWN); 276 | } else { 277 | EMIT_MOVE(moves, sq + 8, sq, BLACK_PAWN, EMPTY_FLAG); 278 | } 279 | } 280 | while (p2) { 281 | POP_LSB(sq, p2); 282 | EMIT_MOVE(moves, sq + 16, sq, BLACK_PAWN, EMPTY_FLAG); 283 | } 284 | while (a1) { 285 | POP_LSB(sq, a1); 286 | if (test_bit(promo, sq)) { 287 | EMIT_PROMOTIONS(moves, sq + 7, sq, BLACK_PAWN); 288 | } else { 289 | if (test_bit(board->ep, sq)) { 290 | EMIT_EN_PASSANT(moves, sq + 7, sq, BLACK_PAWN); 291 | } else { 292 | EMIT_MOVE(moves, sq + 7, sq, BLACK_PAWN, EMPTY_FLAG); 293 | } 294 | } 295 | } 296 | while (a2) { 297 | POP_LSB(sq, a2); 298 | if (test_bit(promo, sq)) { 299 | EMIT_PROMOTIONS(moves, sq + 9, sq, BLACK_PAWN); 300 | } else { 301 | if (test_bit(board->ep, sq)) { 302 | EMIT_EN_PASSANT(moves, sq + 9, sq, BLACK_PAWN); 303 | } else { 304 | EMIT_MOVE(moves, sq + 9, sq, BLACK_PAWN, EMPTY_FLAG); 305 | } 306 | } 307 | } 308 | 309 | return moves - ptr; 310 | } 311 | 312 | INLINE int gen_black_knight_moves(ChessBoard *board, Move *moves) { 313 | return gen_knight_moves(moves, board->bb_squares[BLACK_KNIGHT], 314 | ~board->occ[BLACK], BLACK); 315 | } 316 | 317 | INLINE int gen_black_bishop_moves(ChessBoard *board, Move *moves) { 318 | return gen_bishop_moves(moves, board->bb_squares[BLACK_BISHOP], 319 | ~board->occ[BLACK], board->occ[BOTH], BLACK); 320 | } 321 | 322 | INLINE int gen_black_rook_moves(ChessBoard *board, Move *moves) { 323 | return gen_rook_moves(moves, board->bb_squares[BLACK_ROOK], 324 | ~board->occ[BLACK], board->occ[BOTH], BLACK); 325 | } 326 | 327 | INLINE int gen_black_queen_moves(ChessBoard *board, Move *moves) { 328 | return gen_queen_moves(moves, board->bb_squares[BLACK_QUEEN], 329 | ~board->occ[BLACK], board->occ[BOTH], BLACK); 330 | } 331 | 332 | INLINE int gen_black_king_moves(ChessBoard *board, Move *moves) { 333 | return gen_king_moves(moves, board->bb_squares[BLACK_KING], 334 | ~board->occ[BLACK], BLACK); 335 | } 336 | 337 | int gen_black_king_castle(ChessBoard *board, Move *moves) { 338 | Move *ptr = moves; 339 | if (board->castle & CASTLE_BLACK_KING_SIDE) { 340 | if (!(board->occ[BOTH] & 0x6000000000000000L)) { 341 | Move dummy[MAX_MOVES]; 342 | bb mask = 0x3000000000000000L; 343 | if (!(gen_white_attacks_against(board, dummy, mask))) { 344 | EMIT_CASTLE(moves, 60, 62, BLACK_KING); 345 | } 346 | } 347 | } 348 | if (board->castle & CASTLE_BLACK_QUEEN_SIDE) { 349 | if (!(board->occ[BOTH] & 0x0e00000000000000L)) { 350 | Move dummy[MAX_MOVES]; 351 | bb mask = 0x1800000000000000L; 352 | if (!(gen_white_attacks_against(board, dummy, mask))) { 353 | EMIT_CASTLE(moves, 60, 58, BLACK_KING); 354 | } 355 | } 356 | } 357 | return moves - ptr; 358 | } 359 | 360 | INLINE int gen_black_moves(ChessBoard *board, Move *moves) { 361 | static const MoveGen black_generators[] = { 362 | gen_black_pawn_moves, gen_black_knight_moves, gen_black_bishop_moves, 363 | gen_black_rook_moves, gen_black_queen_moves, gen_black_king_moves, 364 | gen_black_king_castle 365 | }; 366 | 367 | Move *ptr = moves; 368 | for (size_t i = 0; i < sizeof(black_generators) / sizeof(black_generators[0]); 369 | i++) { 370 | moves += black_generators[i](board, moves); 371 | } 372 | 373 | return moves - ptr; 374 | } 375 | 376 | int gen_black_pawn_attacks_against(ChessBoard *board, Move *moves, bb mask) { 377 | Move *ptr = moves; 378 | bb pawns = board->bb_squares[BLACK_PAWN]; 379 | bb a1 = ((pawns & 0x7f7f7f7f7f7f7f7fL) >> 7) & mask; 380 | bb a2 = ((pawns & 0xfefefefefefefefeL) >> 9) & mask; 381 | int sq; 382 | 383 | while (a1) { 384 | POP_LSB(sq, a1); 385 | EMIT_MOVE(moves, sq + 7, sq, BLACK_PAWN, EMPTY_FLAG); 386 | } 387 | 388 | while (a2) { 389 | POP_LSB(sq, a2); 390 | EMIT_MOVE(moves, sq + 9, sq, BLACK_PAWN, EMPTY_FLAG); 391 | } 392 | 393 | return moves - ptr; 394 | } 395 | 396 | INLINE int gen_black_knight_attacks_against(ChessBoard *board, Move *moves, 397 | bb mask) { 398 | return gen_knight_moves(moves, board->bb_squares[BLACK_KNIGHT], mask, BLACK); 399 | } 400 | 401 | INLINE int gen_black_bishop_attacks_against(ChessBoard *board, Move *moves, 402 | bb mask) { 403 | return gen_bishop_moves(moves, board->bb_squares[BLACK_BISHOP], mask, 404 | board->occ[BOTH], BLACK); 405 | } 406 | 407 | INLINE int gen_black_rook_attacks_against(ChessBoard *board, Move *moves, 408 | bb mask) { 409 | return gen_rook_moves(moves, board->bb_squares[BLACK_ROOK], mask, 410 | board->occ[BOTH], BLACK); 411 | } 412 | 413 | INLINE int gen_black_queen_attacks_against(ChessBoard *board, Move *moves, 414 | bb mask) { 415 | return gen_queen_moves(moves, board->bb_squares[BLACK_QUEEN], mask, 416 | board->occ[BOTH], BLACK); 417 | } 418 | 419 | INLINE int gen_black_king_attacks_agianst(ChessBoard *board, Move *moves, 420 | bb mask) { 421 | return gen_king_moves(moves, board->bb_squares[BLACK_KING], mask, BLACK); 422 | } 423 | 424 | INLINE int gen_black_attacks_against(ChessBoard *board, Move *moves, bb mask) { 425 | static const AttacksGen black_attack_generators[] = { 426 | gen_black_pawn_attacks_against, gen_black_knight_attacks_against, 427 | gen_black_bishop_attacks_against, gen_black_rook_attacks_against, 428 | gen_black_queen_attacks_against, gen_black_king_attacks_agianst 429 | }; 430 | 431 | Move *ptr = moves; 432 | for (size_t i = 0; 433 | i < sizeof(black_attack_generators) / sizeof(black_attack_generators[0]); 434 | i++) { 435 | moves += black_attack_generators[i](board, moves, mask); 436 | } 437 | 438 | return moves - ptr; 439 | } 440 | 441 | INLINE int gen_black_checks(ChessBoard *board, Move *moves) { 442 | return gen_black_attacks_against(board, moves, board->bb_squares[WHITE_KING]); 443 | } 444 | 445 | INLINE int gen_moves(ChessBoard *board, Move *moves) { 446 | return board->color ? gen_black_moves(board, moves) 447 | : gen_white_moves(board, moves); 448 | } 449 | 450 | INLINE int gen_attacks(ChessBoard *board, Move *moves) { 451 | return board->color 452 | ? gen_black_attacks_against(board, moves, board->occ[WHITE]) 453 | : gen_white_attacks_against(board, moves, board->occ[BLACK]); 454 | } 455 | 456 | INLINE int gen_legal_moves(ChessBoard *board, Move *moves) { 457 | Move temp[MAX_MOVES]; 458 | Undo undo; 459 | int count = gen_moves(board, temp), size = 0; 460 | for (int i = 0; i < count; i++) { 461 | Move move = temp[i]; 462 | do_move(board, move, &undo); 463 | if (!illegal_to_move(board)) 464 | moves[size++] = move; 465 | undo_move(board, move, &undo); 466 | } 467 | return size; 468 | } 469 | 470 | INLINE int illegal_to_move(ChessBoard *board) { 471 | return board->color 472 | ? attacks_to_king_square(board, board->bb_squares[WHITE_KING]) 473 | : attacks_to_king_square(board, board->bb_squares[BLACK_KING]); 474 | } 475 | 476 | INLINE int is_check(ChessBoard *board) { 477 | Move moves[MAX_MOVES]; 478 | return board->color ? gen_white_checks(board, moves) 479 | : gen_black_checks(board, moves); 480 | } 481 | 482 | int move_gives_check(ChessBoard *board, const Move move) { 483 | Undo undo; 484 | 485 | do_move(board, move, &undo); 486 | int flag = is_check(board); 487 | undo_move(board, move, &undo); 488 | 489 | return flag; 490 | } -------------------------------------------------------------------------------- /sisyphus/gen.h: -------------------------------------------------------------------------------- 1 | #ifndef GEN_H 2 | #define GEN_H 3 | 4 | #include "attacks.h" 5 | #include "bb.h" 6 | #include "board.h" 7 | #include "move.h" 8 | 9 | /* Macros */ 10 | #define EMIT_MOVE(m, from, to, piece, flag) \ 11 | *(m++) = ENCODE_MOVE(from, to, piece, EMPTY_FLAG); // Emit normal move 12 | 13 | #define EMIT_PROMOTION(m, from, to, piece, flag) \ 14 | *(m++) = ENCODE_MOVE(from, to, piece, flag) // Emit promotion move 15 | 16 | #define EMIT_PROMOTIONS(m, from, to, piece) \ 17 | for (int flag = KNIGHT_PROMO_FLAG; flag <= QUEEN_PROMO_FLAG; \ 18 | EMIT_PROMOTION(m, from, to, piece, flag), flag++) // Emit all promotion types 19 | 20 | #define EMIT_EN_PASSANT(m, from, to, piece) \ 21 | *(m++) = ENCODE_MOVE(from, to, piece, ENP_FLAG); // Emit en passant move 22 | 23 | #define EMIT_CASTLE(m, from, to, piece) \ 24 | *(m++) = ENCODE_MOVE(from, to, piece, CATLE_FLAG); // Emit castling move 25 | 26 | // White piece move generation 27 | int gen_white_moves(ChessBoard *board, Move *moves); // Generate all white moves 28 | int gen_white_pawn_moves(ChessBoard *board, Move *moves); // Generate white pawn moves 29 | int gen_white_knight_moves(ChessBoard *board, Move *moves); // Generate white knight moves 30 | int gen_white_bishop_moves(ChessBoard *board, Move *moves); // Generate white bishop moves 31 | int gen_white_rook_moves(ChessBoard *board, Move *moves); // Generate white rook moves 32 | int gen_white_queen_moves(ChessBoard *board, Move *moves); // Generate white queen moves 33 | int gen_white_king_moves(ChessBoard *board, Move *moves); // Generate white king moves 34 | int gen_white_attacks_against(ChessBoard *board, Move *moves, bb mask); // Generate white attacks to squares 35 | 36 | // Black piece move generation 37 | int gen_black_moves(ChessBoard *board, Move *moves); // Generate all black moves 38 | int gen_black_pawn_moves(ChessBoard *board, Move *moves); // Generate black pawn moves 39 | int gen_black_knight_moves(ChessBoard *board, Move *moves); // Generate black knight moves 40 | int gen_black_bishop_moves(ChessBoard *board, Move *moves); // Generate black bishop moves 41 | int gen_black_rook_moves(ChessBoard *board, Move *moves); // Generate black rook moves 42 | int gen_black_queen_moves(ChessBoard *board, Move *moves); // Generate black queen moves 43 | int gen_black_king_moves(ChessBoard *board, Move *moves); // Generate black king moves 44 | int gen_black_attacks_against(ChessBoard *board, Move *moves, bb mask); // Generate black attacks to squares 45 | 46 | // General move generation 47 | int gen_knight_moves(Move *moves, bb srcs, bb mask, int color); // Generate knight moves for given squares 48 | int gen_attacks(ChessBoard *board, Move *moves); // Generate all attacking moves 49 | int gen_legal_moves(ChessBoard *board, Move *moves); // Generate all legal moves 50 | int gen_moves(ChessBoard *board, Move *moves); // Generate all possible moves 51 | 52 | // Move validation and check detection 53 | int illegal_to_move(ChessBoard *board); // Check if position is illegal 54 | int is_check(ChessBoard *board); // Check if king is in check 55 | int move_gives_check(ChessBoard *board, const Move move); // Check if move gives check 56 | 57 | #endif // GEN_H -------------------------------------------------------------------------------- /sisyphus/move.c: -------------------------------------------------------------------------------- 1 | #include "move.h" 2 | 3 | int History_Heuristic[COLOR_NB][SQUARE_NB][SQUARE_NB] = {0}; 4 | Move KILLER_MOVES[COLOR_NB][64] = {0}; 5 | 6 | const char *PROMOTION_TO_CHAR = "-nbrq-"; 7 | 8 | // from https://rustic-chess.org/search/ordering/mvv_lva.html 9 | const int MVV_LVA[6][6] = { 10 | [KING] = {0, 0, 0, 0, 0, 0}, [QUEEN] = {55, 54, 53, 52, 51, 50}, 11 | [ROOK] = {45, 44, 43, 42, 41, 40}, [BISHOP] = {35, 34, 33, 32, 31, 30}, 12 | [KNIGHT] = {25, 24, 23, 22, 21, 20}, [PAWN] = {15, 14, 13, 12, 11, 10}, 13 | }; 14 | 15 | const char *SQ_TO_COORD[64] = { 16 | "h8", "g8", "f8", "e8", "d8", "c8", "b8", "a8", "h7", "g7", "f7", 17 | "e7", "d7", "c7", "b7", "a7", "h6", "g6", "f6", "e6", "d6", "c6", 18 | "b6", "a6", "h5", "g5", "f5", "e5", "d5", "c5", "b5", "a5", "h4", 19 | "g4", "f4", "e4", "d4", "c4", "b4", "a4", "h3", "g3", "f3", "e3", 20 | "d3", "c3", "b3", "a3", "h2", "g2", "f2", "e2", "d2", "c2", "b2", 21 | "a2", "h1", "g1", "f1", "e1", "d1", "c1", "b1", "a1", 22 | }; 23 | 24 | void make_move(ChessBoard *board, Move move) { 25 | Undo undo; 26 | do_move(board, move, &undo); 27 | } 28 | 29 | bool is_capture(ChessBoard *board, const Move move) { 30 | return (bool)(board->squares[EXTRACT_TO(move)] != NONE); 31 | } 32 | 33 | bool is_tactical_move(ChessBoard *board, const Move move) { 34 | int flag = EXTRACT_FLAGS(move); 35 | return is_capture(board, move) || IS_ENP(flag) || IS_PROMO(flag); 36 | } 37 | 38 | void do_move(ChessBoard *board, Move move, Undo *undo) { 39 | int src = EXTRACT_FROM(move); 40 | int dst = EXTRACT_TO(move); 41 | int piece = EXTRACT_PIECE(move); 42 | int color = COLOR(piece); 43 | int flag = EXTRACT_FLAGS(move); 44 | 45 | ASSERT((src >= 0 && src < SQUARE_NB) && (dst >= 0 && dst < SQUARE_NB)); 46 | ASSERT(piece >= WHITE_PAWN && piece <= NONE); 47 | ASSERT(color == WHITE || color == BLACK); 48 | ASSERT(flag >= EMPTY_FLAG && flag <= QUEEN_PROMO_FLAG); 49 | 50 | board->m_history[board->numMoves++] = board->hash; 51 | 52 | TOGGLE_HASH(board); 53 | 54 | undo->capture = board->squares[dst]; 55 | undo->ep = board->ep; 56 | undo->castle = board->castle; 57 | 58 | board_update(board, src, NONE); 59 | 60 | if (!IS_PROMO(flag)) { 61 | board_update(board, dst, piece); 62 | } 63 | 64 | board->ep = U64(0); 65 | 66 | if (piece == WHITE_PAWN) { 67 | bb bsrc = BIT(src); 68 | bb bdst = BIT(dst); 69 | if ((bsrc & RANK_2) && (bdst & RANK_4)) { 70 | board->ep = BIT(src + 8); 71 | } 72 | if (IS_ENP(flag)) { 73 | board_update(board, dst - 8, NONE); 74 | } 75 | HANDLE_PROMOTION(board, piece, flag, dst, color); 76 | } else if (piece == BLACK_PAWN) { 77 | bb bsrc = BIT(src); 78 | bb bdst = BIT(dst); 79 | if ((bsrc & RANK_7) && (bdst & RANK_5)) { 80 | board->ep = BIT(src - 8); 81 | } 82 | if (IS_ENP(flag)) { 83 | board_update(board, dst + 8, NONE); 84 | } 85 | HANDLE_PROMOTION(board, piece, flag, dst, color); 86 | } else if (piece == WHITE_KING) { 87 | board->castle &= ~CASTLE_WHITE; 88 | if (IS_CAS(flag)) { 89 | if (src == 4 && dst == 6) { 90 | board_update(board, 7, NONE); 91 | board_update(board, 5, WHITE_ROOK); 92 | } else if (src == 4 && dst == 2) { 93 | board_update(board, 0, NONE); 94 | board_update(board, 3, WHITE_ROOK); 95 | } 96 | } 97 | } else if (piece == BLACK_KING) { 98 | board->castle &= ~CASTLE_BLACK; 99 | if (IS_CAS(flag)) { 100 | if (src == 60 && dst == 62) { 101 | board_update(board, 63, NONE); 102 | board_update(board, 61, BLACK_ROOK); 103 | } else if (src == 60 && dst == 58) { 104 | board_update(board, 56, NONE); 105 | board_update(board, 59, BLACK_ROOK); 106 | } 107 | } 108 | } 109 | 110 | if (board->castle) { 111 | board->castle &= ~castling_rights[src]; 112 | board->castle &= ~castling_rights[dst]; 113 | } 114 | 115 | SWITCH_SIDE(board); 116 | board->hash ^= HASH_COLOR_SIDE; 117 | TOGGLE_HASH(board); 118 | } 119 | 120 | void undo_move(ChessBoard *board, Move move, Undo *undo) { 121 | int piece = EXTRACT_PIECE(move); 122 | int src = EXTRACT_FROM(move); 123 | int dst = EXTRACT_TO(move); 124 | int flag = EXTRACT_FLAGS(move); 125 | 126 | ASSERT((src >= 0 && src < SQUARE_NB) && (dst >= 0 && dst < SQUARE_NB)); 127 | ASSERT(piece >= WHITE_PAWN && piece <= NONE); 128 | ASSERT(flag >= EMPTY_FLAG && flag <= QUEEN_PROMO_FLAG); 129 | 130 | TOGGLE_HASH(board); 131 | 132 | int capture = undo->capture; 133 | 134 | board->ep = undo->ep; 135 | board->castle = undo->castle; 136 | 137 | board_update(board, src, piece); 138 | 139 | board_update(board, dst, capture); 140 | 141 | if (piece == WHITE_PAWN) { 142 | if (IS_ENP(flag)) { 143 | board_update(board, dst - 8, BLACK_PAWN); 144 | } 145 | } else if (piece == BLACK_PAWN) { 146 | if (IS_ENP(flag)) { 147 | board_update(board, dst + 8, WHITE_PAWN); 148 | } 149 | } 150 | 151 | else if (piece == WHITE_KING) { 152 | if (IS_CAS(flag)) { 153 | if (src == 4 && dst == 6) { 154 | board_update(board, 7, WHITE_ROOK); 155 | board_update(board, 5, NONE); 156 | } else if (src == 4 && dst == 2) { 157 | board_update(board, 0, WHITE_ROOK); 158 | board_update(board, 3, NONE); 159 | } 160 | } 161 | } else if (piece == BLACK_KING) { 162 | if (IS_CAS(flag)) { 163 | if (src == 60 && dst == 62) { 164 | board_update(board, 63, BLACK_ROOK); 165 | board_update(board, 61, NONE); 166 | } else if (src == 60 && dst == 58) { 167 | board_update(board, 56, BLACK_ROOK); 168 | board_update(board, 59, NONE); 169 | } 170 | } 171 | } 172 | SWITCH_SIDE(board); 173 | board->hash ^= HASH_COLOR_SIDE; 174 | TOGGLE_HASH(board); 175 | board->numMoves--; 176 | } 177 | 178 | void do_null_move_pruning(ChessBoard *board, Undo *undo) { 179 | board->m_history[board->numMoves++] = board->hash; 180 | TOGGLE_HASH(board); 181 | undo->ep = board->ep; 182 | board->ep = U64(0); 183 | SWITCH_SIDE(board); 184 | board->hash ^= HASH_COLOR_SIDE; 185 | TOGGLE_HASH(board); 186 | } 187 | 188 | void undo_null_move_pruning(ChessBoard *board, Undo *undo) { 189 | TOGGLE_HASH(board); 190 | board->ep = undo->ep; 191 | SWITCH_SIDE(board); 192 | board->hash ^= HASH_COLOR_SIDE; 193 | TOGGLE_HASH(board); 194 | board->numMoves--; 195 | } 196 | 197 | void score_moves(ChessBoard *board, Move move, int *score) { 198 | int piece = EXTRACT_PIECE(move); 199 | int src = EXTRACT_FROM(move), dst = EXTRACT_TO(move), 200 | flag = EXTRACT_FLAGS(move); 201 | int color = COLOR(piece), result = 0; 202 | int attacker = PIECE(EXTRACT_PIECE(move)), 203 | victim = PIECE(board->squares[dst]); 204 | 205 | result = 206 | (color == WHITE) 207 | ? square_values[piece][(dst)] - square_values[piece][(src)] 208 | : square_values[piece][FLIP(dst)] - square_values[piece][FLIP(src)]; 209 | 210 | if (is_capture(board, move)) 211 | result += MVV_LVA[victim][attacker] + 10000; 212 | 213 | if (IS_PROMO(flag)) 214 | result += (color == WHITE) ? square_values[PROMO_PT(flag)][dst] - 215 | square_values[WHITE_PAWN][dst] 216 | : square_values[PROMO_PT(flag)][FLIP(dst)] - 217 | square_values[BLACK_PAWN][FLIP(dst)]; 218 | else if (IS_ENP(flag)) 219 | result += MVV_LVA[PAWN][PAWN] + 10000; 220 | 221 | *score = result; 222 | } 223 | 224 | INLINE int move_estimated_value(ChessBoard *board, Move move) { 225 | int flag = EXTRACT_FLAGS(move); 226 | int value = SEEPieceValues[PIECE(board->squares[EXTRACT_TO(move)])]; 227 | 228 | if (IS_PROMO(flag)) 229 | value += SEEPieceValues[PROMO_PT(flag)] - SEEPieceValues[PAWN]; 230 | 231 | else if (IS_ENP(flag)) 232 | value = SEEPieceValues[PAWN]; 233 | 234 | else if (IS_CAS(flag)) 235 | value = 0; 236 | 237 | return value; 238 | } 239 | 240 | char *move_to_str(Move move) { 241 | static char buffer[6]; 242 | int src = EXTRACT_FROM(move), dst = EXTRACT_TO(move); 243 | int flag = EXTRACT_FLAGS(move); 244 | if (IS_PROMO(flag)) { 245 | sprintf(buffer, "%s%s%c", SQ_TO_COORD[FLIP_63(src)], 246 | SQ_TO_COORD[FLIP_63(dst)], PROMOTION_TO_CHAR[PROMO_PT(flag)]); 247 | } else { 248 | sprintf(buffer, "%s%s", SQ_TO_COORD[FLIP_63(src)], 249 | SQ_TO_COORD[FLIP_63(dst)]); 250 | } 251 | return buffer; 252 | } -------------------------------------------------------------------------------- /sisyphus/move.h: -------------------------------------------------------------------------------- 1 | #ifndef MOVE_H 2 | #define MOVE_H 3 | 4 | #include "bb.h" 5 | #include "board.h" 6 | #include "gen.h" 7 | #include "zobrist.h" 8 | #include 9 | #include 10 | 11 | // Constants 12 | #define MAX_MOVES 218 // Maximum possible moves in any position 13 | 14 | // Move flags for different types of moves 15 | #define EMPTY_FLAG 0 // Normal move 16 | #define ENP_FLAG 6 // En passant capture 17 | #define CAPTURE_FLAG 4 // Regular capture 18 | #define CATLE_FLAG 1 // Castling move 19 | #define PROMO_FLAG 8 // Base promotion flag 20 | #define KNIGHT_PROMO_FLAG 8 // Promote to knight 21 | #define ROOK_PROMO_FLAG 9 // Promote to rook 22 | #define BISHOP_PROMO_FLAG 10 // Promote to bishop 23 | #define QUEEN_PROMO_FLAG 11 // Promote to queen 24 | 25 | // Move flag checking macros 26 | #define NULL_MOVE U32(0) // Represents no move 27 | #define IS_PROMO(flag) ((bool)((flag) & PROMO_FLAG)) // Check if move is promotion 28 | #define PROMO_PT(flag) ((flag & 0x3) + KNIGHT) // Get promotion piece type 29 | #define IS_ENP(flag) ((flag) == ENP_FLAG) // Check if en passant 30 | #define IS_CAS(flag) ((flag) == CATLE_FLAG) // Check if castling 31 | 32 | // Board update macros 33 | #define HANDLE_PROMOTION(board, piece, flag, dst, color) \ 34 | if (IS_PROMO(flag)) { \ 35 | board_update(board, dst, make_piece_type(PROMO_PT(flag), color)); \ 36 | } 37 | 38 | #define TOGGLE_HASH(board) \ 39 | if (board->castle) \ 40 | board->hash ^= HASH_CASTLE[board->castle]; \ 41 | if (board->ep) \ 42 | board->hash ^= HASH_EP[get_lsb(board->ep) % 8]; 43 | 44 | // Move encoding format (32 bits): 45 | // from (6 bits) | to (6 bits) | piece (4 bits) | flags (4 bits) 46 | #define ENCODE_MOVE(from, to, piece, flag) \ 47 | (((from) | ((to) << 6) | ((piece) << 12) | ((flag) << 16))) 48 | 49 | // Move decoding macros 50 | #define EXTRACT_FROM(move) ((int)(((move) >> 0) & 0x3f)) // Get source square 51 | #define EXTRACT_TO(move) ((int)(((move) >> 6) & 0x3f)) // Get target square 52 | #define EXTRACT_FLAGS(move) ((int)(((move) >> 16) & 0xf)) // Get move flags 53 | #define EXTRACT_PIECE(move) ((int)(((move) >> 12) & 0xf)) // Get piece type 54 | 55 | // Move scoring tables 56 | extern int History_Heuristic[COLOR_NB][SQUARE_NB][SQUARE_NB]; 57 | extern Move KILLER_MOVES[COLOR_NB][SQUARE_NB]; 58 | extern const int MVV_LVA[6][6]; 59 | static const int SEEPieceValues[] = { 60 | 103, 422, 437, 694, 1313, 0, 0, 0, 61 | }; 62 | 63 | // Move manipulation functions 64 | void do_move(ChessBoard *board, Move move, Undo *undo); // Make a move 65 | void make_move(ChessBoard *board, Move move); // Make move without undo 66 | void undo_move(ChessBoard *board, Move move, Undo *undo); // Take back a move 67 | 68 | // Move scoring and evaluation 69 | void score_moves(ChessBoard *board, Move move, int *score); // Score moves for ordering 70 | int move_estimated_value(ChessBoard *board, Move move); // Estimate move value 71 | 72 | // Null move pruning 73 | void undo_null_move_pruning(ChessBoard *board, Undo *undo); // Undo null move 74 | void do_null_move_pruning(ChessBoard *board, Undo *undo); // Make null move 75 | 76 | // Utility functions 77 | char *move_to_str(Move move); // Convert move to string 78 | bool is_capture(ChessBoard *board, const Move move); // Check if move is capture 79 | bool is_tactical_move(ChessBoard *board, const Move move); // Check if tactical move 80 | 81 | #endif // MOVE_H -------------------------------------------------------------------------------- /sisyphus/search.c: -------------------------------------------------------------------------------- 1 | #include "search.h" 2 | #include "utils.h" 3 | 4 | #define FullDepthMoves 5 5 | #define ReductionLimit 3 6 | #define MAX_PLY 100 7 | 8 | void sort_moves(Search *search, ChessBoard *board, Move *moves, int count, 9 | int ply) { 10 | Move temp[MAX_MOVES]; 11 | int scores[MAX_MOVES]; 12 | int indexes[MAX_MOVES]; 13 | 14 | Move best = table_get_move(&search->table, board->hash); 15 | for (int i = 0; i < count; i++) { 16 | Move move = moves[i]; 17 | score_moves(board, move, &scores[i]); 18 | if (!is_capture(board, move)) { 19 | if (best != NULL_MOVE && (best == move)) 20 | scores[i] += INF; 21 | else if (KILLER_MOVES[WHITE][ply] == move) 22 | scores[i] += 9000; 23 | else if (KILLER_MOVES[BLACK][ply] == move) 24 | scores[i] += 8000; 25 | else 26 | scores[i] += 27 | History_Heuristic[board->color][EXTRACT_FROM(move)][EXTRACT_TO(move)]; 28 | } 29 | indexes[i] = i; 30 | } 31 | 32 | for (int i = 1; i < count; i++) { 33 | int j = i; 34 | while (j > 0 && scores[j - 1] < scores[j]) { 35 | swap_any(&scores[j - 1], &scores[j], sizeof(int)); 36 | swap_any(&indexes[j - 1], &indexes[j], sizeof(int)); 37 | j--; 38 | } 39 | } 40 | 41 | memcpy(temp, moves, sizeof(Move) * count); 42 | 43 | for (int i = 0; i < count; i++) { 44 | moves[i] = temp[indexes[i]]; 45 | } 46 | } 47 | 48 | int ok_to_reduce(ChessBoard *board, Move move) { 49 | // https://www.chessprogramming.org/Late_Move_Reductions#Uncommon_Conditions 50 | return ((!is_tactical_move(board, move)) && (!move_gives_check(board, move))); 51 | } 52 | 53 | int quiescence_search(Search *search, ChessBoard *board, int ply, int alpha, 54 | int beta) { 55 | int score, count; 56 | Undo undo; 57 | Move moves[MAX_MOVES]; 58 | 59 | score = eval(board); 60 | 61 | if (score >= beta) { 62 | return beta; 63 | } 64 | 65 | // https://www.chessprogramming.org/Delta_Pruning 66 | int Delta = 975; // queen value 67 | if (score + Delta < alpha) { 68 | return alpha; 69 | } 70 | 71 | alpha = MAX(alpha, score); 72 | 73 | count = gen_attacks(board, moves); 74 | sort_moves(search, board, moves, count, ply); 75 | 76 | for (int i = 0; i < count; i++) { 77 | Move move = moves[i]; 78 | 79 | if (!staticExchangeEvaluation(board, move, 0)) 80 | continue; 81 | search->nodes++; 82 | do_move(board, move, &undo); 83 | int value = -quiescence_search(search, board, ply + 1, -beta, -alpha); 84 | undo_move(board, move, &undo); 85 | 86 | if (search->stop) { 87 | alpha = 0; 88 | goto stop_loop; 89 | } 90 | 91 | if (value >= beta) { 92 | return beta; 93 | } 94 | 95 | alpha = MAX(alpha, value); 96 | } 97 | 98 | stop_loop: 99 | return alpha; 100 | } 101 | 102 | int negamax(Search *search, ChessBoard *board, int depth, int ply, int alpha, 103 | int beta, bool cutnode) { 104 | int flag = ALPHA, value = -INF, count, TtHit, can_move = 0, 105 | moves_searched = 0; 106 | const int isPv = (alpha != beta - 1); 107 | const int isRootN = (ply != 0); 108 | const int InCheck = is_check(board); 109 | Undo undo; 110 | Move moves[MAX_MOVES]; 111 | 112 | depth = MAX(depth, 0); // Make sure depth >= 0 113 | 114 | if (!isRootN) { 115 | if (is_draw(board, ply)) 116 | return 0; 117 | if (ply >= MAX_PLY) 118 | return InCheck ? 0 : eval(board); 119 | int rAlpha = MAX(alpha, -MATE + ply); 120 | int rBeta = MIN(beta, MATE - ply - 1); 121 | if (rAlpha >= rBeta) 122 | return rAlpha; 123 | } 124 | 125 | if ((TtHit = table_get(&search->table, board->hash, depth, alpha, beta, 126 | &value))) { 127 | return value; 128 | } 129 | 130 | // https://www.chessprogramming.org/Internal_Iterative_Reductions 131 | if (!InCheck) { 132 | Entry *entry = table_entry(&search->table, board->hash); 133 | if ((isPv || cutnode) && depth >= 4 && entry->move == NULL_MOVE) 134 | depth--; 135 | } 136 | 137 | if (depth == 0 && !InCheck) { 138 | value = quiescence_search(search, board, ply, alpha, beta); 139 | table_set(&search->table, board->hash, depth, value, EXACT); 140 | return value; 141 | } 142 | 143 | value = eval(board); 144 | 145 | // Reverse Futility Pruning 146 | if (!InCheck && !isPv && depth <= 3) { 147 | int margine = 150 * depth; 148 | if (value >= beta + margine) 149 | return (value + beta) / 150 | 2; // https://www.chessprogramming.org/Reverse_Futility_Pruning#Return_Value 151 | } 152 | 153 | // Razoring 154 | if (!InCheck && !isPv) { 155 | if (depth <= 5 && value + 214 * depth <= alpha) { 156 | int score = quiescence_search(search, board, ply, alpha, beta); 157 | if (score <= alpha) 158 | return score; 159 | } 160 | } 161 | 162 | // Extended Null-Move Reductions 163 | if (!InCheck && !isPv && depth >= 3) { 164 | do_null_move_pruning(board, &undo); 165 | int R = depth > 6 ? MAX_R : MIN_R; 166 | int score = -negamax(search, board, depth - R - 1, ply + 1, -beta, 167 | -beta + 1, !cutnode); 168 | undo_null_move_pruning(board, &undo); 169 | if (score >= beta) { 170 | depth -= DR; 171 | if (depth <= 0) 172 | return quiescence_search(search, board, ply, alpha, beta); 173 | table_set(&search->table, board->hash, depth, beta, BETA); 174 | return beta; 175 | } 176 | } 177 | 178 | count = gen_legal_moves(board, moves); 179 | sort_moves(search, board, moves, count, ply); 180 | 181 | for (int i = 0; i < count; i++) { 182 | Move move = moves[i]; 183 | do_move(board, move, &undo); 184 | if (moves_searched == 0) { 185 | value = 186 | -negamax(search, board, depth - 1, ply + 1, -beta, -alpha, !cutnode); 187 | } else { 188 | if (moves_searched >= FullDepthMoves && depth >= ReductionLimit && 189 | !isPv && !is_check(board) && 190 | !staticExchangeEvaluation(board, move, 0) && 191 | ok_to_reduce(board, move) && 192 | KILLER_MOVES[board->color][ply] != move) { 193 | value = -negamax(search, board, depth - 2, ply + 1, -alpha - 1, -alpha, 194 | true); 195 | } else { 196 | value = alpha + 1; 197 | } 198 | 199 | if (value > alpha) { 200 | value = -negamax(search, board, depth - 1, ply + 1, -alpha - 1, -alpha, 201 | !cutnode); 202 | if (value > alpha && value < beta) { 203 | value = -negamax(search, board, depth - 1, ply + 1, -beta, -alpha, 204 | !cutnode); 205 | } 206 | } 207 | } 208 | undo_move(board, move, &undo); 209 | 210 | moves_searched++; 211 | 212 | if (search->stop) { 213 | alpha = 0; 214 | goto stop_loop; 215 | } 216 | 217 | if (value > -INF) 218 | can_move = 1; 219 | 220 | if (value >= beta) { 221 | // Store Killer moves 222 | if (!is_capture(board, move)) { 223 | KILLER_MOVES[BLACK][ply] = KILLER_MOVES[WHITE][ply]; 224 | KILLER_MOVES[WHITE][ply] = move; 225 | } 226 | table_set(&search->table, board->hash, depth, beta, BETA); 227 | table_set_move(&search->table, board->hash, depth, move); 228 | return beta; 229 | } 230 | 231 | if (value > alpha) { 232 | if (!is_capture(board, move)) { 233 | History_Heuristic[board->color][EXTRACT_FROM(move)][EXTRACT_TO(move)] += 234 | depth * depth; 235 | } 236 | flag = EXACT; 237 | alpha = value; 238 | table_set_move(&search->table, board->hash, depth, move); 239 | } 240 | } 241 | 242 | if (!can_move) 243 | return is_check(board) ? -MATE + ply : 0; 244 | 245 | table_set(&search->table, board->hash, depth, alpha, flag); 246 | 247 | stop_loop: 248 | return alpha; 249 | } 250 | 251 | int staticExchangeEvaluation(ChessBoard *board, Move move, int threshold) { 252 | int src = EXTRACT_FROM(move); 253 | int dst = EXTRACT_TO(move); 254 | int flag = EXTRACT_FLAGS(move); 255 | int piece = EXTRACT_PIECE(move); 256 | 257 | if (IS_CAS(flag) || IS_ENP(flag) || IS_PROMO(flag)) 258 | return 1; 259 | 260 | int value = move_estimated_value(board, move) - threshold; 261 | if (value < 0) 262 | return 0; 263 | 264 | value = SEEPieceValues[piece] - value; 265 | if (value <= 0) 266 | return 1; 267 | 268 | int color = board->color; 269 | bb occ = board->occ[BOTH] ^ BIT(src) ^ BIT(dst); 270 | bb attackers = attacks_to_square(board, dst, occ); 271 | bb mine, leastattacker; 272 | 273 | const bb diag = 274 | board->bb_squares[WHITE_BISHOP] | board->bb_squares[BLACK_BISHOP] | 275 | board->bb_squares[WHITE_QUEEN] | board->bb_squares[BLACK_QUEEN]; 276 | const bb straight = 277 | board->bb_squares[WHITE_ROOK] | board->bb_squares[BLACK_ROOK] | 278 | board->bb_squares[WHITE_QUEEN] | board->bb_squares[BLACK_QUEEN]; 279 | 280 | int result = 1; 281 | 282 | while (1) { 283 | color ^= BLACK; 284 | attackers &= occ; 285 | 286 | if (!(mine = (attackers & board->occ[color]))) 287 | break; 288 | 289 | result &= BLACK; 290 | 291 | if ((leastattacker = 292 | mine & board->bb_squares[make_piece_type(PAWN, color)])) { 293 | if ((value = SEEPieceValues[PAWN] - value) < result) 294 | break; 295 | occ ^= (leastattacker & -leastattacker); 296 | attackers |= get_bishop_attacks(dst, occ); 297 | } else if ((leastattacker = 298 | mine & board->bb_squares[make_piece_type(KNIGHT, color)])) { 299 | if ((value = SEEPieceValues[KNIGHT] - value) < result) 300 | break; 301 | occ ^= (leastattacker & -leastattacker); 302 | } else if ((leastattacker = mine & make_piece_type(BISHOP, color))) { 303 | if ((value = SEEPieceValues[BISHOP] - value) < result) 304 | break; 305 | 306 | occ ^= (leastattacker & -leastattacker); 307 | attackers |= get_bishop_attacks(dst, occ) & diag; 308 | } else if ((leastattacker = mine & make_piece_type(ROOK, color))) { 309 | if ((value = SEEPieceValues[ROOK] - value) < result) 310 | break; 311 | 312 | occ ^= (leastattacker & -leastattacker); 313 | attackers |= get_rook_attacks(dst, occ) & straight; 314 | } else if ((leastattacker = mine & make_piece_type(QUEEN, color))) { 315 | if ((value = SEEPieceValues[QUEEN] - value) < result) 316 | break; 317 | 318 | occ ^= (leastattacker & -leastattacker); 319 | attackers |= (get_bishop_attacks(dst, occ) & diag) | 320 | (get_rook_attacks(dst, occ) & straight); 321 | } else 322 | return (attackers & ~board->occ[color]) ? result ^ BLACK : result; 323 | } 324 | return result; 325 | } 326 | 327 | int root_search(Search *search, ChessBoard *board, int depth, int alpha, 328 | int beta, Move *result) { 329 | Move best_move = NULL_MOVE; 330 | Move moves[MAX_MOVES]; 331 | Undo undo; 332 | int count = gen_legal_moves(board, moves), can_move = 0; 333 | sort_moves(search, board, moves, count, 1); 334 | 335 | for (int i = 0; i < count; i++) { 336 | Move move = moves[i]; 337 | 338 | search->nodes++; 339 | do_move(board, move, &undo); 340 | int score = -negamax(search, board, depth - 1, 1, -beta, -alpha, false); 341 | undo_move(board, move, &undo); 342 | 343 | if (search->stop) { 344 | alpha = 0; 345 | goto stop_loop; 346 | } 347 | 348 | if (score > alpha) { 349 | alpha = score; 350 | best_move = move; 351 | can_move = 1; 352 | } 353 | } 354 | 355 | stop_loop: 356 | if (can_move) { 357 | *result = best_move; 358 | table_set_move(&search->table, board->hash, depth, best_move); 359 | } 360 | return alpha; 361 | } 362 | 363 | void print_pv(Search *search, ChessBoard *board, int depth) { 364 | if (depth <= 0) 365 | return; 366 | 367 | Entry *entry = table_entry(&search->table, board->hash); 368 | 369 | if (entry->key != board->hash) 370 | return; 371 | 372 | Move move = entry->move; 373 | Undo undo; 374 | 375 | if (move != NULL_MOVE) { 376 | printf(" %s", move_to_str(move)); 377 | do_move(board, move, &undo); 378 | print_pv(search, board, depth - 1); 379 | undo_move(board, move, &undo); 380 | } 381 | } 382 | 383 | int best_move(Search *search, ChessBoard *board, Move *result, bool debug) { 384 | int best_score = -INF; 385 | int alpha = -INF, beta = INF; 386 | search->stop = false; 387 | 388 | if (!table_alloc(&search->table, 20)) { 389 | return -best_score; 390 | } 391 | 392 | memset(History_Heuristic, 0, sizeof(History_Heuristic)); 393 | 394 | for (int depth = 1; depth <= MAX_DEPTH; depth++) { 395 | best_score = root_search(search, board, depth, alpha, beta, result); 396 | 397 | // Aspiration window https://www.frayn.net/beowulf/theory.html#aspiration 398 | if ((best_score <= alpha) || (best_score >= beta)) { 399 | alpha = -INF; 400 | beta = INF; 401 | continue; 402 | } 403 | 404 | alpha = best_score - VALID_WINDOW; 405 | beta = best_score + VALID_WINDOW; 406 | 407 | if (debug) { 408 | printf("info score=%d, depth=%d, pv ", best_score, depth); 409 | print_pv(search, board, depth); 410 | printf("\n"); 411 | } 412 | 413 | if (best_score == -INF) { 414 | best_score = 0; 415 | goto cleanup; 416 | } 417 | 418 | if (search->stop) 419 | goto cleanup; 420 | 421 | if (best_score >= MATE - depth || best_score <= -MATE + depth) 422 | goto cleanup; 423 | } 424 | 425 | cleanup: 426 | table_free(&search->table); 427 | return best_score; 428 | } -------------------------------------------------------------------------------- /sisyphus/search.h: -------------------------------------------------------------------------------- 1 | #ifndef SEARCH_H 2 | #define SEARCH_H 3 | 4 | #include "attacks.h" 5 | #include "bb.h" 6 | #include "eval.h" 7 | #include "gen.h" 8 | #include "move.h" 9 | #include "table.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define INF 1000000 17 | #define MATE 100000 18 | 19 | #define MAX_DEPTH 100 20 | #define VALID_WINDOW 50 21 | 22 | #define MAX_R 4 23 | #define MIN_R 3 24 | #define DR 4 25 | 26 | int best_move(Search *search, ChessBoard *board, Move *result, bool debug); 27 | 28 | int staticExchangeEvaluation(ChessBoard *board, Move move, int threshold); 29 | 30 | #endif // SEARCH_H -------------------------------------------------------------------------------- /sisyphus/table.c: -------------------------------------------------------------------------------- 1 | #include "table.h" 2 | 3 | Entry *table_entry(Table *table, bb key) { 4 | return &table->entry[key & table->mask]; 5 | }; 6 | 7 | void table_free(Table *table) { 8 | free(table->entry); 9 | }; 10 | 11 | void table_prefetch(Table *table, bb key) { 12 | __builtin_prefetch(table_entry(table, key)); 13 | }; 14 | 15 | void table_clear(Table *table) { 16 | memset(table, 0, sizeof(Table)); 17 | } 18 | 19 | int table_alloc(Table *table, int bits) { 20 | table_clear(table); 21 | table->size = 1 << bits; 22 | table->mask = table->size - 1; 23 | table->entry = calloc(table->size, sizeof(Entry)); 24 | if (table->entry == NULL) { 25 | err("table_alloc(): failed to allocate transposition table entries"); 26 | return 0; 27 | } 28 | return 1; 29 | } 30 | 31 | Move table_get_move(Table *table, bb key) { 32 | Entry *entry = table_entry(table, key); 33 | if (entry->key == key) { 34 | return entry->move; 35 | } 36 | 37 | return 0; 38 | } 39 | 40 | void table_set_move(Table *table, bb key, int depth, Move move) { 41 | Entry *entry = table_entry(table, key); 42 | if (entry->depth <= depth) { 43 | entry->key = key; 44 | entry->depth = depth; 45 | entry->move = move; 46 | } 47 | } 48 | 49 | void table_set(Table *table, bb key, int depth, int value, int flag) { 50 | Entry *entry = table_entry(table, key); 51 | if (entry->depth <= depth) { 52 | entry->key = key; 53 | entry->depth = depth; 54 | entry->score = value; 55 | entry->flag = flag; 56 | } 57 | } 58 | 59 | int table_get(Table *table, bb key, int depth, int alpha, int beta, 60 | int *value) { 61 | Entry *entry = table_entry(table, key); 62 | int flag = 0; 63 | if (entry->key == key) { 64 | if (entry->depth >= depth) { 65 | if (entry->flag == EXACT) { 66 | *value = entry->score; 67 | flag = 1; 68 | } 69 | if ((entry->flag == ALPHA) && (entry->score <= alpha)) { 70 | *value = alpha; 71 | flag = 1; 72 | } 73 | if ((entry->flag == BETA) && (entry->score >= beta)) { 74 | *value = beta; 75 | flag = 1; 76 | } 77 | } 78 | } 79 | return flag; 80 | } 81 | 82 | int pawn_table_alloc(PawnTable *table, int bits) { 83 | table->size = 1 << bits; 84 | table->mask = table->size - 1; 85 | table->entry = calloc(table->size, sizeof(PawnEntry)); 86 | 87 | if (table->entry == NULL) { 88 | err("pawn_table_alloc(): failed to allocate pawn transposition table " 89 | "entries"); 90 | return 0; 91 | } 92 | return 1; 93 | } 94 | 95 | INLINE PawnEntry *pawn_table_entry(PawnTable *table, bb key) { 96 | return &table->entry[key & table->mask]; 97 | } 98 | 99 | void pawn_table_set(PawnTable *table, bb key, int value) { 100 | PawnEntry *entry = pawn_table_entry(table, key); 101 | entry->key = key; 102 | entry->value = value; 103 | } 104 | 105 | int pawn_table_get(PawnTable *tabke, bb key) { 106 | PawnEntry *entry = pawn_table_entry(tabke, key); 107 | if (entry->key == key) 108 | return entry->value; 109 | return 0; 110 | } 111 | 112 | void pawn_table_free(PawnTable *table) { 113 | return free(table->entry); 114 | } -------------------------------------------------------------------------------- /sisyphus/table.h: -------------------------------------------------------------------------------- 1 | #ifndef TABLE_H 2 | #define TABLE_H 3 | 4 | #include "board.h" 5 | #include "move.h" 6 | #include "types.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Node types in transposition table 13 | #define EXACT 1 // Exact evaluation score 14 | #define ALPHA 2 // Upper bound score 15 | #define BETA 3 // Lower bound score 16 | 17 | // Function declarations for table operations 18 | // Prefetch table entry for given position key 19 | void table_prefetch(Table *table, bb key); 20 | 21 | // Allocate memory for transposition table 22 | int table_alloc(Table *table, int bits); 23 | 24 | // Free table memory 25 | void table_free(Table *table); 26 | 27 | // Get table entry for a position 28 | Entry *table_entry(Table *table, bb key); 29 | 30 | // Get stored move from table 31 | Move table_get_move(Table *table, bb key); 32 | 33 | // Store move in table 34 | void table_set_move(Table *table, bb key, int depth, Move move); 35 | 36 | // Store position evaluation in table 37 | void table_set(Table *table, bb key, int depth, int value, int flag); 38 | 39 | // Retrieve position evaluation from table 40 | int table_get(Table *table, bb key, int depth, int alpha, int beta, int *value); 41 | 42 | #endif -------------------------------------------------------------------------------- /sisyphus/types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | #include 5 | #include 6 | 7 | #define INLINE inline __attribute__((always_inline)) 8 | 9 | // Debug print macro 10 | #if !defined(DEBUG_DISABLE_PRINT) || defined(DEBUG) 11 | #define err(str) \ 12 | fprintf(stderr, "%s, at %s, line %d\n", str, __FILE__, __LINE__); \ 13 | fflush(stderr) 14 | #else 15 | #define err(str) 16 | #endif 17 | 18 | // Assertion macro 19 | #if !defined(DISABLE_ASSERT) 20 | #define ASSERT(expr) assert(expr) 21 | #else 22 | #define ASSERT(expr) ((void)0) 23 | #endif 24 | 25 | typedef unsigned long long bb; // Bitboard type definition (64-bit unsigned integer) 26 | 27 | #define U64(u) u##ULL 28 | #define U32(u) u##U 29 | 30 | typedef struct { 31 | int squares[64]; // Piece placement array 32 | int numMoves; // Number of moves played 33 | int color; // Current side to move 34 | int castle; // Castling rights 35 | 36 | int mg[2]; // Middlegame evaluation score for both sides 37 | int eg[2]; // Endgame evaluation score for both sides 38 | int gamePhase; // Current game phase (opening/middlegame/endgame) 39 | 40 | bb bb_squares[12]; // Bitboards for each piece type and color 41 | bb m_history[8192]; // Move history for repetition detection 42 | bb occ[3]; // Occupancy bitboards (white/black/both) 43 | 44 | bb ep; // En passant square bitboard 45 | bb hash; // Position hash 46 | bb pawn_hash; // Pawn structure hash (not used) 47 | } ChessBoard; 48 | 49 | typedef uint32_t Move; // Move type (32-bit unsigned integer) 50 | 51 | typedef int (*MoveGen)(ChessBoard *, Move *); 52 | typedef int (*AttacksGen)(ChessBoard *, Move *, bb); 53 | 54 | typedef struct { 55 | int capture; // Captured piece 56 | int castle; // Previous castling rights 57 | bb ep; // Previous en passant square 58 | } Undo; 59 | 60 | typedef struct { 61 | bb key; // Position hash 62 | int score; // Evaluation score 63 | int depth; // Search depth 64 | int flag; // Entry type flag 65 | Move move; // Best move 66 | } Entry; 67 | 68 | typedef struct { 69 | int size; // Table size 70 | int mask; // Size mask for indexing 71 | Entry *entry; // Array of entries 72 | } Table; 73 | 74 | typedef struct { 75 | bb key; // Pawn structure hash 76 | int value; // Evaluation score 77 | } PawnEntry; 78 | 79 | typedef struct { 80 | int size; // Table size 81 | int mask; // Size mask for indexing 82 | PawnEntry *entry; // Array of entries 83 | } PawnTable; 84 | 85 | typedef struct { 86 | int nodes; // Nodes searched 87 | bool stop; // Search stop flag 88 | Move move; // Best move found 89 | Table table; // Transposition table 90 | } Search; 91 | 92 | typedef struct { 93 | int score; // Evaluation score 94 | bool debug; // Debug flag 95 | Search *search; // Search information 96 | ChessBoard *board; // Board state 97 | Move *move; // Move to be played 98 | } Thread_d; 99 | 100 | typedef struct { 101 | bb key; // Position hash 102 | bb value; // Stored value 103 | int depth; // Search depth 104 | } Entry_t; 105 | 106 | #define SQUARE_NB 64 107 | #define COLOR_NB 2 108 | #define FILE_NB 8 109 | #define RANK_NB 8 110 | 111 | #define PAWN_MATERIAL 100 112 | #define KNIGHT_MATERIAL 320 113 | #define BISHOP_MATERIAL 330 114 | #define ROOK_MATERIAL 500 115 | #define QUEEN_MATERIAL 900 116 | #define KING_MATERIAL 20000 117 | 118 | #define CASTLE_ALL 15 119 | #define CASTLE_WHITE 3 120 | #define CASTLE_BLACK 12 121 | #define CASTLE_WHITE_KING_SIDE 1 122 | #define CASTLE_WHITE_QUEEN_SIDE 2 123 | #define CASTLE_BLACK_KING_SIDE 4 124 | #define CASTLE_BLACK_QUEEN_SIDE 8 125 | 126 | #define WHITE 0 127 | #define BLACK 1 128 | #define BOTH 2 129 | 130 | #define PAWN 0 131 | #define KNIGHT 1 132 | #define BISHOP 2 133 | #define ROOK 3 134 | #define QUEEN 4 135 | #define KING 5 136 | 137 | #define WHITE_PAWN 0 138 | #define BLACK_PAWN 1 139 | #define WHITE_KNIGHT 2 140 | #define BLACK_KNIGHT 3 141 | #define WHITE_BISHOP 4 142 | #define BLACK_BISHOP 5 143 | #define WHITE_ROOK 6 144 | #define BLACK_ROOK 7 145 | #define WHITE_QUEEN 8 146 | #define BLACK_QUEEN 9 147 | #define WHITE_KING 10 148 | #define BLACK_KING 11 149 | #define NONE 12 150 | 151 | #define RANK_1 0x00000000000000ffULL 152 | #define RANK_2 0x000000000000ff00ULL 153 | #define RANK_3 0x0000000000ff0000ULL 154 | #define RANK_4 0x00000000ff000000ULL 155 | #define RANK_5 0x000000ff00000000ULL 156 | #define RANK_6 0x0000ff0000000000ULL 157 | #define RANK_7 0x00ff000000000000ULL 158 | #define RANK_8 0xff00000000000000ULL 159 | 160 | #define FILE_A 0x0101010101010101ULL 161 | #define FILE_B 0x0202020202020202ULL 162 | #define FILE_C 0x0404040404040404ULL 163 | #define FILE_D 0x0808080808080808ULL 164 | #define FILE_E 0x1010101010101010ULL 165 | #define FILE_F 0x2020202020202020ULL 166 | #define FILE_G 0x4040404040404040ULL 167 | #define FILE_H 0x8080808080808080ULL 168 | 169 | #endif // TYPES_H -------------------------------------------------------------------------------- /sisyphus/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | void swap_any(void *a, void *b, size_t s) { 4 | void *temp = malloc(s); 5 | memcpy(temp, a, s); 6 | memcpy(a, b, s); 7 | memcpy(b, temp, s); 8 | free(temp); 9 | } 10 | 11 | char *strdup(const char *src) { 12 | if (src == NULL) 13 | return NULL; 14 | 15 | size_t len = strlen(src) + 1; 16 | void *new_s = malloc(len); 17 | 18 | if (new_s == NULL) 19 | return NULL; 20 | 21 | return (char *)memcpy(new_s, src, len); 22 | } 23 | 24 | bb xorshift64() { 25 | // https://en.wikipedia.org/wiki/Xorshift 26 | // https://vigna.di.unimi.it/ftp/papers/xorshift.pdf 27 | static bb x = U64(1); 28 | x ^= x >> 12; 29 | x ^= x << 25; 30 | x ^= x >> 27; 31 | return x * U64(0x2545F4914F6CDD1D); 32 | } 33 | -------------------------------------------------------------------------------- /sisyphus/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include "types.h" 5 | #include 6 | #include 7 | 8 | // Generic swap function for any data type 9 | void swap_any(void *a, void *b, size_t s); 10 | 11 | // String duplication function 12 | char *strdup(const char *src); 13 | 14 | // Random number generator using xorshift algorithm 15 | bb xorshift64(); 16 | 17 | #endif -------------------------------------------------------------------------------- /sisyphus/zobrist.c: -------------------------------------------------------------------------------- 1 | #include "zobrist.h" 2 | #include "utils.h" 3 | 4 | bb HASH_PIECES[12][64]; 5 | bb HASH_EP[8]; 6 | bb HASH_CASTLE[16]; 7 | bb HASH_COLOR_SIDE; 8 | 9 | void init_zobrist() { 10 | for (int i = 0; i < 12; i++) { 11 | for (int j = 0; j < 64; j++) { 12 | HASH_PIECES[i][j] = xorshift64(); 13 | } 14 | } 15 | 16 | for (int i = 0; i < 8; i++) { 17 | HASH_EP[i] = xorshift64(); 18 | } 19 | 20 | for (int i = 0; i < 16; i++) { 21 | HASH_CASTLE[i] = xorshift64(); 22 | } 23 | HASH_COLOR_SIDE = xorshift64(); 24 | } 25 | 26 | void gen_curr_state_zobrist(ChessBoard *board) { 27 | for (int pc = WHITE_PAWN; pc <= BLACK_KING; pc++) { 28 | bb bbit = board->bb_squares[pc]; 29 | int sq; 30 | 31 | while (bbit) { 32 | POP_LSB(sq, bbit); 33 | board->hash ^= HASH_PIECES[pc][sq]; 34 | } 35 | } 36 | 37 | if (board->color) { 38 | board->hash ^= HASH_COLOR_SIDE; 39 | } 40 | 41 | board->hash ^= HASH_CASTLE[board->castle]; 42 | 43 | if (board->ep) { 44 | board->hash ^= HASH_EP[get_lsb(board->ep) % 8]; 45 | } 46 | } 47 | 48 | void gen_pawn_zobrist(ChessBoard *board) { 49 | 50 | for (int pc = WHITE_PAWN; pc <= BLACK_PAWN; pc++) { 51 | bb bbit = board->bb_squares[pc]; 52 | int sq; 53 | while (bbit) { 54 | POP_LSB(sq, bbit); 55 | board->pawn_hash ^= HASH_PIECES[pc][sq]; 56 | } 57 | } 58 | 59 | if (board->ep) { 60 | board->pawn_hash ^= HASH_EP[get_lsb(board->ep) % 8]; 61 | } 62 | 63 | if (board->color) { 64 | board->pawn_hash ^= HASH_COLOR_SIDE; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sisyphus/zobrist.h: -------------------------------------------------------------------------------- 1 | #ifndef ZOBRIST_H 2 | #define ZOBRIST_H 3 | 4 | #include "bb.h" 5 | #include "types.h" 6 | #include 7 | 8 | // Random hash values for each piece on each square 9 | extern bb HASH_PIECES[12][64]; 10 | 11 | // Hash values for en passant squares 12 | extern bb HASH_EP[8]; 13 | 14 | // Hash values for castling rights 15 | extern bb HASH_CASTLE[16]; 16 | 17 | // Hash value for side to move 18 | extern bb HASH_COLOR_SIDE; 19 | 20 | // Initialize Zobrist hash tables with random values 21 | void init_zobrist(); 22 | 23 | // Generate Zobrist hash for current board state 24 | void gen_curr_state_zobrist(ChessBoard *board); 25 | 26 | // Generate Zobrist hash for pawn structure 27 | void gen_pawn_zobrist(ChessBoard *board); 28 | 29 | #endif -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import unittest 4 | import sisyphus 5 | 6 | 7 | class RaiseLogHandler(logging.StreamHandler): 8 | def handle(self, record): 9 | super().handle(record) 10 | raise RuntimeError("was expecting no log messages") 11 | 12 | 13 | class UtilsTestCase(unittest.TestCase): 14 | def test_square(self): 15 | for square in list(range(sisyphus.SQUARE_NB)): 16 | file_sq = sisyphus.utils.file_of(square) 17 | rank_of = sisyphus.utils.rank_of(square) 18 | self.assertEqual(sisyphus.utils.square(rank_of, file_sq), square) 19 | 20 | def test_bit_operations(self): 21 | bb = sisyphus.utils.bit(sisyphus.C2) 22 | self.assertEqual(sisyphus.utils.get_lsb(bb), 10) 23 | self.assertEqual(sisyphus.utils.popcount(bb), 1) 24 | self.assertTrue(sisyphus.utils.test_bit(bb, 10)) 25 | self.assertFalse(sisyphus.utils.test_bit(bb, 11)) 26 | 27 | def test_square_distances(self): 28 | self.assertEqual(sisyphus.utils.square_distance(sisyphus.A1, sisyphus.H8), 7) 29 | self.assertEqual(sisyphus.utils.square_distance(sisyphus.E4, sisyphus.E5), 1) 30 | self.assertEqual( 31 | sisyphus.utils.square_manhattan_distance(sisyphus.A1, sisyphus.H8), 14 32 | ) 33 | self.assertEqual( 34 | sisyphus.utils.square_manhattan_distance(sisyphus.E4, sisyphus.E5), 1 35 | ) 36 | 37 | def test_scan_forward(self): 38 | bb = (1 << sisyphus.A1) | (1 << sisyphus.C3) | (1 << sisyphus.H8) 39 | squares = list(sisyphus.utils.scan_forward(bb)) 40 | self.assertEqual(squares, [sisyphus.A1, sisyphus.C3, sisyphus.H8]) 41 | 42 | def test_file_and_rank(self): 43 | self.assertEqual(sisyphus.utils.file_of(sisyphus.E4), 4) 44 | self.assertEqual(sisyphus.utils.rank_of(sisyphus.E4), 3) 45 | self.assertEqual(sisyphus.utils.file_of(sisyphus.H8), 7) 46 | self.assertEqual(sisyphus.utils.rank_of(sisyphus.H8), 7) 47 | 48 | 49 | class BoardTestCase(unittest.TestCase): 50 | def test_default_position(self): 51 | board = sisyphus.Board() 52 | self.assertEqual(board.turn, sisyphus.WHITE) 53 | self.assertEqual(board.fen, sisyphus.STARTING_FEN) 54 | self.assertEqual(board.turn, sisyphus.WHITE) 55 | 56 | def test_eq(self): 57 | b1 = sisyphus.Board( 58 | "r1bqkb1r/ppppppp1/2n2n1p/6B1/3P4/2N5/PPP1PPPP/R2QKBNR w KQkq - 0 1" 59 | ) 60 | b2 = sisyphus.Board( 61 | "r1bqkb1r/ppppppp1/2n2n1p/6B1/3P4/2N5/PPP1PPPP/R2QKBNR w KQkq - 0 1" 62 | ) 63 | self.assertEqual(b1, b2) 64 | 65 | def test_fen(self): 66 | test_positions = [ 67 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -", 68 | "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq -", 69 | "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq -", 70 | "8/8/8/8/8/8/8/8 w - -", 71 | ] 72 | 73 | board = sisyphus.Board() 74 | 75 | for fen in test_positions: 76 | board.set_fen(fen) 77 | self.assertEqual(board.fen, fen) 78 | 79 | with self.assertRaises(ValueError): 80 | board.set_fen("") 81 | 82 | with self.assertRaises(TypeError): 83 | board.set_fen(None) 84 | 85 | def test_move_making(self): 86 | board = sisyphus.Board() 87 | count = 0 88 | for move in board.gen_legal_moves: 89 | board.push(move) 90 | for move in board.gen_legal_moves: 91 | board.push(move) 92 | count += 1 93 | board.pop() 94 | board.pop() 95 | self.assertEqual(count, 400) 96 | with self.assertRaises(TypeError): 97 | board.push(None) 98 | 99 | def test_color_at(self): 100 | board = sisyphus.Board() 101 | self.assertEqual(board.color_at(sisyphus.A1), sisyphus.WHITE) 102 | self.assertEqual(board.color_at(sisyphus.G7), sisyphus.BLACK) 103 | self.assertEqual(board.color_at(sisyphus.E4), None) 104 | 105 | def test_clear(self): 106 | board = sisyphus.Board() 107 | moves = board.gen_legal_moves 108 | move = sisyphus.Move( 109 | sisyphus.A2, sisyphus.A3, sisyphus.PieceType(sisyphus.PAWN, sisyphus.WHITE) 110 | ) 111 | board.push(move) 112 | board.clear() 113 | self.assertEqual(board.fen, sisyphus.STARTING_FEN) 114 | self.assertEqual(len(board._handle_moves), 0) 115 | 116 | def test_perft(self): 117 | test_positions = { 118 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": [ 119 | (1, 20), 120 | (2, 400), 121 | (3, 8902), 122 | (4, 197281), 123 | (5, 4865609), 124 | (6, 119060324), 125 | ], 126 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ": [ 127 | (1, 48), 128 | (2, 2039), 129 | (3, 97862), 130 | (4, 4085603), 131 | (5, 193690690), 132 | ], 133 | "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1": [ 134 | (1, 6), 135 | (2, 264), 136 | (3, 9467), 137 | (4, 422333), 138 | (5, 15833292), 139 | ], 140 | "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - ": [ 141 | (1, 14), 142 | (2, 191), 143 | (3, 2812), 144 | (4, 43238), 145 | (5, 674624), 146 | (6, 11030083), 147 | (7, 178633661), 148 | ], 149 | "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8": [ 150 | (1, 44), 151 | (2, 1486), 152 | (3, 62379), 153 | (4, 2103487), 154 | ], 155 | "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10": [ 156 | (1, 46), 157 | (2, 2079), 158 | (3, 89890), 159 | (4, 3894594), 160 | (5, 164075551), 161 | ], 162 | } 163 | 164 | board = sisyphus.Board() 165 | 166 | max_test_depth = 3 167 | for fen, expected_results in test_positions.items(): 168 | board.set_fen(fen) 169 | 170 | for depth, expected_nodes in expected_results: 171 | if depth <= max_test_depth: 172 | nodes = board.perft_test(depth) 173 | self.assertEqual( 174 | nodes, 175 | expected_nodes, 176 | f"Perft failed at depth {depth} for position {fen}\n" 177 | f"Expected {expected_nodes} nodes, got {nodes}", 178 | ) 179 | 180 | with self.assertRaises(ValueError): 181 | board.perft_test(0) 182 | with self.assertRaises(ValueError): 183 | board.perft_test(-1) 184 | 185 | def test_attackers(self): 186 | board = sisyphus.Board( 187 | "r1b1k2r/pp1n1ppp/2p1p3/q5B1/1b1P4/P1n1PN2/1P1Q1PPP/2R1KB1R b Kkq - 3 10" 188 | ) 189 | 190 | attackers = board.attackers(sisyphus.WHITE, sisyphus.C3) 191 | self.assertEqual(len(attackers), 3) 192 | self.assertIn(sisyphus.C1, attackers) 193 | self.assertIn(sisyphus.D2, attackers) 194 | self.assertIn(sisyphus.B2, attackers) 195 | self.assertNotIn(sisyphus.D4, attackers) 196 | self.assertNotIn(sisyphus.E1, attackers) 197 | 198 | def test_check(self): 199 | board = sisyphus.Board( 200 | "rnbqkbnr/ppp2ppp/3p4/1B2Q3/8/8/PPPPPPPP/RN2KBNR b KQkq - 0 1" 201 | ) 202 | self.assertTrue(board.is_check()) 203 | 204 | def test_checkers(self): 205 | board = sisyphus.Board( 206 | "rnbqkbnr/ppp2ppp/3p4/1B2Q3/8/8/PPPPPPPP/RN2KBNR b KQkq - 0 1" 207 | ) 208 | mask = board.checkers(sisyphus.BLACK) 209 | self.assertEqual(len(mask), 2) 210 | 211 | def test_checkmate(self): 212 | board = sisyphus.Board() 213 | self.assertFalse(board.is_checkmate()) 214 | board.set_fen("rnbqkbnr/ppp2ppp/3p4/1B2Q3/8/8/PPPPPPPP/RN2KBNR b KQkq - 0 1") 215 | self.assertTrue(board.is_checkmate()) 216 | 217 | def test_castling_rights(self): 218 | board = sisyphus.Board() 219 | self.assertEqual(board.castling_rights(), sisyphus.CASTLE_ALL) 220 | 221 | board = sisyphus.Board("1q1k4/2Rr4/8/2Q3K1/8/8/8/8 w - - 0 1") 222 | self.assertEqual(board.castling_rights(), sisyphus.EMPTY_FLAG) 223 | 224 | 225 | def test_peek(self): 226 | board = sisyphus.Board() 227 | move = sisyphus.Move( 228 | sisyphus.A2, sisyphus.A3, sisyphus.PieceType(sisyphus.PAWN, sisyphus.WHITE) 229 | ) 230 | board.push(move) 231 | self.assertEqual(board.peek(), move) 232 | board.pop() 233 | 234 | self.assertEqual(board.peek(), None) 235 | 236 | 237 | class BaseBoardTestCase(unittest.TestCase): 238 | def test_equal(self): 239 | a1 = sisyphus.BaseBoard() 240 | a2 = sisyphus.BaseBoard() 241 | self.assertEqual(a1, a2) 242 | 243 | def test_init(self): 244 | board = sisyphus.BaseBoard() 245 | 246 | for c in range(sisyphus.BOTH): 247 | self.assertEqual(len(board.pawns(c)), 8) 248 | self.assertEqual(len(board.knights(c)), 2) 249 | self.assertEqual(len(board.bishops(c)), 2) 250 | self.assertEqual(len(board.rooks(c)), 2) 251 | self.assertEqual(len(board.queens(c)), 1) 252 | self.assertEqual(len(board.kings(c)), 1) 253 | 254 | def test_king_sq(self): 255 | board = sisyphus.BaseBoard() 256 | self.assertEqual(board.king_sq(sisyphus.WHITE), sisyphus.E1) 257 | self.assertEqual(board.king_sq(sisyphus.BLACK), sisyphus.E8) 258 | 259 | def test_piece_at(self): 260 | board = sisyphus.BaseBoard() 261 | self.assertEqual(board.piece_at(sisyphus.D1), sisyphus.PieceType.from_symbol("q")) 262 | self.assertEqual(board.piece_at(sisyphus.F8), sisyphus.PieceType.from_symbol("B")) 263 | self.assertEqual(board.piece_at(sisyphus.D3), None) 264 | 265 | with self.assertRaises(IndexError): 266 | board.piece_at(-1) 267 | 268 | def test_color_at(self): 269 | board = sisyphus.BaseBoard() 270 | self.assertEqual(board.color_at(sisyphus.A2), sisyphus.WHITE) 271 | self.assertEqual(board.color_at(sisyphus.A7), sisyphus.BLACK) 272 | 273 | 274 | class SquareSetTestCase(unittest.TestCase): 275 | def test_len(self): 276 | mask = sisyphus.utils.bit(sisyphus.A1) | sisyphus.utils.bit(sisyphus.B1) 277 | a1 = sisyphus.SquareSet(mask) 278 | self.assertTrue(bool(a1)) 279 | self.assertEqual(len(a1), 2) 280 | 281 | def test_contains(self): 282 | mask = sisyphus.utils.bit(sisyphus.A5) 283 | a1 = sisyphus.SquareSet(mask) 284 | 285 | self.assertTrue(sisyphus.A5 in a1) 286 | self.assertFalse(sisyphus.B1 in a1) 287 | 288 | def test_pop(self): 289 | mask = sisyphus.utils.bit(sisyphus.A5) 290 | a1 = sisyphus.SquareSet(mask) 291 | 292 | self.assertEqual(a1.pop(), sisyphus.A5) 293 | with self.assertRaises(KeyError): 294 | a1.pop() 295 | 296 | 297 | class MoveTestCase(unittest.TestCase): 298 | def test_default(self): 299 | board = sisyphus.Board() 300 | m1 = sisyphus.Move( 301 | sisyphus.A2, sisyphus.A3, sisyphus.PieceType(sisyphus.PAWN, sisyphus.WHITE) 302 | ) 303 | self.assertIn(m1, board.gen_legal_moves) 304 | 305 | board.push(m1) # Do move 306 | board.pop() # Undo move 307 | 308 | def test_parse_uci(self): 309 | board = sisyphus.Board() 310 | m1 = sisyphus.Move.parse_uci(board, "g1f3") 311 | self.assertIn(m1, board.gen_legal_moves) 312 | 313 | with self.assertRaises(ValueError): 314 | sisyphus.Move.parse_uci(board, "") 315 | 316 | def test_san(self): 317 | move = sisyphus.Move( 318 | sisyphus.G1, sisyphus.F3, sisyphus.PieceType(sisyphus.KNIGHT, sisyphus.WHITE) 319 | ) 320 | self.assertEqual(move.san, "g1f3") 321 | 322 | 323 | class PiecTypeTestCase(unittest.TestCase): 324 | def test_symbol(self): 325 | p1 = sisyphus.PieceType(sisyphus.PAWN, sisyphus.WHITE) 326 | p2 = sisyphus.PieceType(sisyphus.KING, sisyphus.WHITE) 327 | p3 = sisyphus.PieceType(sisyphus.QUEEN, sisyphus.BLACK) 328 | 329 | self.assertEqual(p1.symbol(), "P") 330 | self.assertEqual(p2.symbol(), "K") 331 | self.assertEqual(p3.symbol(), "q") 332 | 333 | def test_to_index(self): 334 | piece = sisyphus.PieceType(sisyphus.QUEEN, sisyphus.BLACK) 335 | self.assertEqual(hash(piece), 9) 336 | 337 | def test_from_index(self): 338 | piece = sisyphus.PieceType(sisyphus.QUEEN, sisyphus.BLACK) 339 | self.assertEqual(sisyphus.PieceType.from_index(9), piece) 340 | 341 | def test_from_symbol(self): 342 | piece = sisyphus.PieceType(sisyphus.QUEEN, sisyphus.BLACK) 343 | self.assertEqual(sisyphus.PieceType.from_symbol("Q"), piece) 344 | 345 | 346 | if __name__ == "__main__": 347 | verbosity = sum( 348 | arg.count("v") for arg in sys.argv if all(c == "v" for c in arg.lstrip("-")) 349 | ) 350 | verbosity += sys.argv.count("--verbose") 351 | 352 | if verbosity >= 2: 353 | logging.basicConfig(level=logging.DEBUG) 354 | 355 | raise_log_handler = RaiseLogHandler() 356 | raise_log_handler.setLevel(logging.ERROR) 357 | logging.getLogger().addHandler(raise_log_handler) 358 | 359 | unittest.main() 360 | -------------------------------------------------------------------------------- /tools/uci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import sys 5 | from typing import List, Dict, Any 6 | import sisyphus 7 | from sisyphus import Board, Move, Searcher 8 | 9 | class UCI: 10 | """UCI protocol implementation for chess engine.""" 11 | 12 | def __init__(self): 13 | self.board = Board() 14 | self.searcher = Searcher(self.board) 15 | self.count = 0 16 | self.debug = False 17 | 18 | def print_banner(self) -> None: 19 | """Print engine banner with name and version.""" 20 | banner = """ 21 | ███████ ██ ███████ ██ ██ ██████ ██ ██ ██ ██ ███████ 22 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 23 | ███████ ██ ███████ ████ ██████ ███████ ██ ██ ███████ 24 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 25 | ███████ ██ ███████ ██ ██ ██ ██ ██████ ███████ 26 | """ 27 | print(banner) 28 | print(f"\t\t\t\t\t\tAuthor: {sisyphus.__author__}") 29 | print(f"\t\t\t\t\t\tVersion: {sisyphus.__version__}\n") 30 | 31 | def _position(self, args: List[str]) -> None: 32 | """Handle 'position' command.""" 33 | if not args: 34 | return 35 | 36 | if args[0] == "startpos": 37 | self.board.clear() 38 | args = args[1:] 39 | elif args[0] == "fen": 40 | fen = " ".join(args[1:7]) 41 | self.board.set_fen(fen) 42 | args = args[7:] 43 | 44 | if args and args[0] == "moves": 45 | for move in args[1:]: 46 | self.board.push(Move.parse_uci(self.board, move)) 47 | self.count += 1 48 | 49 | def _go(self, args: List[str]) -> None: 50 | """Handle 'go' command.""" 51 | params: Dict[str, Any] = {} 52 | 53 | i = 0 54 | while i < len(args): 55 | param = args[i] 56 | 57 | if param in ["depth", "movetime", "movestogo", "wtime", "winc", "binc", "btime"]: 58 | i += 1 59 | if i < len(args): 60 | params[param] = int(args[i]) 61 | elif param in ["infinite", "ponder"]: 62 | params[param] = True 63 | 64 | i += 1 65 | 66 | # Start search with parameters 67 | if "movetime" in params: 68 | movetime = params["movetime"] / 1000 69 | best_move = self.searcher.start(time_s=movetime) 70 | 71 | elif "wtime" in params: 72 | wtime = params.get("wtime", 0) / 1000 73 | btime = params.get("btime", 0) / 1000 74 | winc = params.get("winc", 0) / 1000 75 | binc = params.get("binc", 0) / 1000 76 | 77 | if not self.board.turn: 78 | time, inc = wtime, winc 79 | else: 80 | time, inc = btime, binc 81 | 82 | think = min(time / 40 + inc, time / 2 - 1) 83 | if self.count < 3: 84 | think = min(think, 1.0) 85 | 86 | best_move = self.searcher.start(time_s=think) 87 | 88 | elif "depth" in params: 89 | best_move = self.searcher.start(depth=params["depth"]) 90 | else: 91 | best_move = self.searcher.start() 92 | 93 | self.count = 0 94 | print(f"bestmove {best_move.move_str()}") 95 | 96 | def loop(self) -> None: 97 | """Main UCI command loop.""" 98 | 99 | self.print_banner() 100 | 101 | while True: 102 | try: 103 | command = input().strip() 104 | 105 | if not command: 106 | continue 107 | 108 | tokens = command.split() 109 | cmd, args = tokens[0], tokens[1:] 110 | 111 | if cmd == "quit": 112 | break 113 | 114 | elif cmd == "stop": 115 | self.searcher.stop() 116 | 117 | elif cmd == "uci": 118 | print("id name Sisyphus") 119 | print(f"id author {sisyphus.__author__}") 120 | print("uciok") 121 | 122 | elif cmd == "isready": 123 | print("readyok") 124 | 125 | elif cmd == "ucinewgame": 126 | self.board.clear() 127 | self.searcher.clear() 128 | 129 | elif cmd == "position": 130 | self._position(args) 131 | 132 | elif cmd == "go": 133 | self._go(args) 134 | 135 | elif cmd == "debug": 136 | self.debug = args[0] == "on" if args else not self.debug 137 | 138 | except Exception as e: 139 | print(f"info string Error: {e}", file=sys.stderr) 140 | 141 | def main() -> None: 142 | """Entry point for UCI interface.""" 143 | uci = UCI() 144 | uci.loop() 145 | 146 | if __name__ == "__main__": 147 | main() --------------------------------------------------------------------------------