From 2b34be89b7229afe8a8f610fcce5178ebfee66e3 Mon Sep 17 00:00:00 2001 From: noobpwnftw Date: Tue, 18 Dec 2018 19:27:04 +0800 Subject: [PATCH] Introduce opening book support My study have shown that it does no harm to use an opening book instead of a search, while the latter being largely deterministic. This is a primitive version, using a PolyGlot-like book format but with our own Zobrist hash keys, so the probing might be cheap enough to be used elsewhere in search. The burden is on the creation of the book to provide more diversity to the openings and cover some corner cases, which is another area of research. No functional changes without a book. --- src/Makefile | 3 +- src/book.cpp | 188 +++++++++++++++++++++++++++++++++++++++++ src/book.h | 53 ++++++++++++ src/main.cpp | 2 + src/misc.h | 30 +++++++ src/search.cpp | 29 ++++++- src/syzygy/tbprobe.cpp | 29 ------- src/ucioption.cpp | 5 ++ 8 files changed, 305 insertions(+), 34 deletions(-) create mode 100644 src/book.cpp create mode 100644 src/book.h diff --git a/src/Makefile b/src/Makefile index 8b9b16e470c..aa33a0274f4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -38,7 +38,8 @@ PGOBENCH = ./$(EXE) bench ### Object files OBJS = benchmark.o bitbase.o bitboard.o endgame.o evaluate.o main.o \ material.o misc.o movegen.o movepick.o pawns.o position.o psqt.o \ - search.o thread.o timeman.o tt.o uci.o ucioption.o syzygy/tbprobe.o + search.o thread.o timeman.o tt.o uci.o ucioption.o book.o \ + syzygy/tbprobe.o ### Establish the operating system name KERNEL = $(shell uname -s) diff --git a/src/book.cpp b/src/book.cpp new file mode 100644 index 00000000000..e8278efffe4 --- /dev/null +++ b/src/book.cpp @@ -0,0 +1,188 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2008 Tord Romstad (Glaurung author) + Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad + Copyright (C) 2015-2019 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "misc.h" +#include "book.h" +#include "movegen.h" +#include +#include + +Book Books; + +Book::Book() +{ + max_book_ply = 400; + NumBookEntries = 0; + BookEntries = nullptr; +} + +Book::~Book() +{ + if (BookEntries != nullptr) + delete[] BookEntries; +} + +void Book::init(const std::string& filename) +{ + if (filename.length() == 0) return; + const char *fnam = filename.c_str(); + + if (strcmp(fnam, "") == 0) + { + return; + } + + FILE *fpt = fopen(fnam, "rb"); + if (fpt == NULL) + { + sync_cout << "info string Could not open " << filename << sync_endl; + return; + } + + if (BookEntries != nullptr) + { + free(BookEntries); + BookEntries = nullptr; + } + + fseek(fpt, 0L, SEEK_END); + int filesize = ftell(fpt); + fseek(fpt, 0L, SEEK_SET); + + NumBookEntries = filesize / sizeof(BookEntry); + BookEntries = new BookEntry[NumBookEntries]; + + size_t nRead = fread(BookEntries, 1, filesize, fpt); + fclose(fpt); + + if (nRead != 1) + { + free(BookEntries); + BookEntries = nullptr; + sync_cout << "info string Could not read " << filename << sync_endl; + return; + } + for (size_t i = 0; i < NumBookEntries; i++) + byteswap_bookentry(&BookEntries[i]); + + sync_cout << "info string Book loaded: " << filename << " (" << NumBookEntries << " entries)" << sync_endl; +} + +void Book::set_max_ply(int new_max_ply) +{ + max_book_ply = new_max_ply; +} + +Move Book::probe_root(Position& pos) +{ + if (BookEntries != nullptr && pos.game_ply() < max_book_ply) + { + int count = 0; + size_t index = find_first_entry(pos.key(), count); + if (count > 0) + { + PRNG rng(now()); + Move move = reconstruct_move(BookEntries[index + rng.rand() % count].move); + + // Add 'special move' flags and verify it is legal + for (const auto& m : MoveList(pos)) + { + if (move == (m.move & (~(3 << 14)))) // compare with MoveType (bit 14-15) masked out + return m; + } + } + } + return MOVE_NONE; +} + +void Book::probe(Position& pos, std::vector& bookmoves) +{ + if (BookEntries != nullptr && pos.game_ply() < max_book_ply) + { + int count = 0; + size_t index = find_first_entry(pos.key(), count); + if (count > 0) + { + for (int i = 0; i < count; i++) + bookmoves.push_back(Move(BookEntries[index + i].move)); + } + } +} + +Move Book::reconstruct_move(uint16_t book_move) +{ + Move move = Move(book_move); + + int pt = (move >> 12) & 7; + if (pt) + return make(from_sq(move), to_sq(move), PieceType(pt + 1)); + + return move; +} + +size_t Book::find_first_entry(uint64_t key, int& index_count) +{ + size_t start = 0; + size_t end = NumBookEntries; + + for (;;) + { + size_t mid = (end + start) / 2; + + if (BookEntries[mid].key < key) + start = mid; + else + { + if (BookEntries[mid].key > key) + end = mid; + else + { + start = std::max(mid - 4, (size_t)0); + end = std::min(mid + 4, NumBookEntries); + } + } + + if (end - start < 9) + break; + } + + for (size_t i = start; i < end; i++) + { + if (key == BookEntries[i].key) + { + while ((i > 0) && (key == BookEntries[i - 1].key)) + i--; + index_count = 1; + end = i; + while ((++end < NumBookEntries) && (key == BookEntries[end].key)) + index_count++; + return i; + } + } + return 0; +} + +void Book::byteswap_bookentry(BookEntry *be) +{ + be->key = number(&be->key); + be->move = number(&be->move); + be->weight = number(&be->weight); + be->score = number(&be->score); +} diff --git a/src/book.h b/src/book.h new file mode 100644 index 00000000000..9225d0b1bd4 --- /dev/null +++ b/src/book.h @@ -0,0 +1,53 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2008 Tord Romstad (Glaurung author) + Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad + Copyright (C) 2015-2019 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef BOOK_H_INCLUDED +#define BOOK_H_INCLUDED + +#include "position.h" +#include + +class Book +{ + typedef struct { + uint64_t key; + uint16_t move; + uint16_t weight; + uint32_t score; + } BookEntry; +public: + Book(); + ~Book(); + void init(const std::string& filename); + void set_max_ply(int new_max_ply); + Move probe_root(Position& pos); + void probe(Position& pos, std::vector& bookmoves); +private: + size_t find_first_entry(uint64_t key, int& index_count); + Move reconstruct_move(uint16_t book_move); + size_t NumBookEntries; + BookEntry *BookEntries; + void byteswap_bookentry(BookEntry *be); + int max_book_ply; +}; + +extern Book Books; + +#endif // #ifndef BOOK_H_INCLUDED diff --git a/src/main.cpp b/src/main.cpp index a093b5bf0a2..03782eb3c88 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,6 +27,7 @@ #include "tt.h" #include "uci.h" #include "syzygy/tbprobe.h" +#include "book.h" namespace PSQT { void init(); @@ -44,6 +45,7 @@ int main(int argc, char* argv[]) { Search::init(); Pawns::init(); Threads.set(Options["Threads"]); + Books.init(Options["BookFile"]); Search::clear(); // After threads are up UCI::loop(argc, argv); diff --git a/src/misc.h b/src/misc.h index 3cba486796b..c355a7968fe 100644 --- a/src/misc.h +++ b/src/misc.h @@ -26,6 +26,7 @@ #include #include #include +#include // For std::memset and std::memcpy #include "types.h" @@ -111,4 +112,33 @@ namespace WinProcGroup { void bindThisThread(size_t idx); } +enum { BigEndian, LittleEndian }; +template +inline void swap_endian(T& x) +{ + static_assert(std::is_unsigned::value, "Argument of swap_endian not unsigned"); + + uint8_t tmp, *c = (uint8_t*)&x; + for (int i = 0; i < Half; ++i) + tmp = c[i], c[i] = c[End - i], c[End - i] = tmp; +} +template<> inline void swap_endian(uint8_t&) {} + +template T number(void* addr) +{ + static const union { uint32_t i; char c[4]; } Le = { 0x01020304 }; + static const bool IsLittleEndian = (Le.c[0] == 4); + + T v; + + if ((uintptr_t)addr & (alignof(T) - 1)) // Unaligned pointer (very rare) + std::memcpy(&v, addr, sizeof(T)); + else + v = *((T*)addr); + + if (LE != IsLittleEndian) + swap_endian(v); + return v; +} + #endif // #ifndef MISC_H_INCLUDED diff --git a/src/search.cpp b/src/search.cpp index 756b2d5769f..f5c4b4528bc 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -25,6 +25,7 @@ #include #include +#include "book.h" #include "evaluate.h" #include "misc.h" #include "movegen.h" @@ -216,11 +217,24 @@ void MainThread::search() { } else { - for (Thread* th : Threads) - if (th != this) - th->start_searching(); + Move bookMove = MOVE_NONE; + + if (!Limits.infinite && !Limits.mate) + bookMove = Books.probe_root(rootPos); + + if (bookMove && std::count(rootMoves.begin(), rootMoves.end(), bookMove)) + { + for (Thread* th : Threads) + std::swap(th->rootMoves[0], *std::find(th->rootMoves.begin(), th->rootMoves.end(), bookMove)); + } + else + { + for (Thread* th : Threads) + if (th != this) + th->start_searching(); - Thread::search(); // Let's start searching! + Thread::search(); // Let's start searching! + } } // When we reach the maximum depth, we can arrive here without a raise of @@ -884,6 +898,10 @@ namespace { contHist, countermove, ss->killers); + + std::vector bookMoves; + Books.probe(pos, bookMoves); + value = bestValue; // Workaround a bogus 'uninitialized' warning under gcc skipQuiets = false; @@ -907,6 +925,9 @@ namespace { thisThread->rootMoves.begin() + thisThread->pvLast, move)) continue; + if (bookMoves.size() && !std::count(bookMoves.begin(), bookMoves.end(), move & (~(3 << 14)))) + continue; + ss->moveCount = ++moveCount; if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index 60979a56874..bbf6e0ce225 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -57,7 +57,6 @@ namespace { constexpr int TBPIECES = 7; // Max number of supported pieces -enum { BigEndian, LittleEndian }; enum TBType { KEY, WDL, DTZ }; // Used as template parameter // Each table has a set of flags: all of them refer to DTZ tables, the last one to WDL tables @@ -90,34 +89,6 @@ constexpr Value WDL_to_value[] = { VALUE_MATE - MAX_PLY - 1 }; -template -inline void swap_endian(T& x) -{ - static_assert(std::is_unsigned::value, "Argument of swap_endian not unsigned"); - - uint8_t tmp, *c = (uint8_t*)&x; - for (int i = 0; i < Half; ++i) - tmp = c[i], c[i] = c[End - i], c[End - i] = tmp; -} -template<> inline void swap_endian(uint8_t&) {} - -template T number(void* addr) -{ - static const union { uint32_t i; char c[4]; } Le = { 0x01020304 }; - static const bool IsLittleEndian = (Le.c[0] == 4); - - T v; - - if ((uintptr_t)addr & (alignof(T) - 1)) // Unaligned pointer (very rare) - std::memcpy(&v, addr, sizeof(T)); - else - v = *((T*)addr); - - if (LE != IsLittleEndian) - swap_endian(v); - return v; -} - // DTZ tables don't store valid scores for moves that reset the rule50 counter // like captures and pawn moves but we can easily recover the correct dtz of the // previous move if we know the position's WDL score. diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 1c6ef777257..e7d0ed2acfa 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -28,6 +28,7 @@ #include "tt.h" #include "uci.h" #include "syzygy/tbprobe.h" +#include "book.h" using std::string; @@ -41,6 +42,8 @@ void on_hash_size(const Option& o) { TT.resize(o); } void on_logger(const Option& o) { start_logger(o); } void on_threads(const Option& o) { Threads.set(o); } void on_tb_path(const Option& o) { Tablebases::init(o); } +void on_book_file(const Option& o) { Books.init(o); } +void on_book_depth(const Option& o) { Books.set_max_ply(o); } /// Our case insensitive less() function as required by UCI protocol @@ -77,6 +80,8 @@ void init(OptionsMap& o) { o["SyzygyProbeDepth"] << Option(1, 1, 100); o["Syzygy50MoveRule"] << Option(true); o["SyzygyProbeLimit"] << Option(7, 0, 7); + o["BookFile"] << Option("", on_book_file); + o["BookDepth"] << Option(400, 1, 400, on_book_depth); }