BitBully 0.0.39
Loading...
Searching...
No Matches
OpeningBook.h
1#ifndef OPENINGBOOK_H
2#define OPENINGBOOK_H
3
4#include <Board.h>
5
6#include <cassert>
7#include <filesystem>
8#include <fstream>
9#include <iostream>
10#include <limits>
11#include <tuple>
12#include <vector>
13
14namespace BitBully {
15
16// TODO: guess database type from size of file!
17class OpeningBook {
18 private:
19 using key_t = int;
20 using value_t = int8_t;
21 static constexpr size_t SIZE_BYTES_8PLY_DB = 103'545; // 102'858;
22 static constexpr size_t SIZE_BYTES_12PLY_DB = 6'943'780;
23 static constexpr size_t SIZE_BYTES_12PLY_DB_WITH_DIST = 21'004'495;
24
25 static constexpr size_t SIZE_8PLY_DB = 34'515;
26 static constexpr size_t SIZE_12PLY_DB = 1'735'945;
27 static constexpr size_t SIZE_12PLY_DB_WITH_DIST = 4'200'899;
28
29 std::vector<std::tuple<key_t, value_t>> m_book;
30 bool m_withDistances{};
31 bool m_is8ply{};
32 std::filesystem::path m_bookPath;
33 int m_nPly{};
34
35 [[nodiscard]] value_t binarySearch(const key_t& huffmanCode) const {
36 // one could also use: std::lower_bound()
37 int l = 0; // dont use size_t to prevent undesired underflows
38 int r = m_book.size() - 1;
39 while (r >= l) {
40 const auto mid = (l + r + 1) / 2;
41 auto p = m_book.at(mid);
42 if (std::get<0>(p) == huffmanCode) {
43 return std::get<1>(p); // Found! return the value for this position
44 }
45 if (std::get<0>(p) > huffmanCode) {
46 r = mid - 1;
47 } else { // p < huffmanCode
48 l = mid + 1;
49 }
50 }
51
52 // Nothing found:
53 return std::numeric_limits<value_t>::min();
54 }
55
56 public:
57 static constexpr auto NONE_VALUE = std::numeric_limits<value_t>::min();
58
59 explicit OpeningBook(const std::filesystem::path& bookPath,
60 const bool is_8ply, const bool with_distances) {
61 init(bookPath, is_8ply, with_distances);
62 }
63
64 explicit OpeningBook(const std::filesystem::path& bookPath) {
65 if (!std::filesystem::exists(bookPath)) {
66 throw std::invalid_argument("Book file does not exist: " +
67 bookPath.string());
68 }
69
70 const auto fileSize = std::filesystem::file_size(bookPath);
71 // infer DB type from size:
72 const bool is8ply = (fileSize == SIZE_BYTES_8PLY_DB);
73 const bool withDistances = (fileSize == SIZE_BYTES_12PLY_DB_WITH_DIST);
74
75 init(bookPath, is8ply, withDistances);
76 }
77
78 auto getBook() const { return m_book; }
79
80 void init(const std::filesystem::path& bookPath, const bool is_8ply,
81 const bool with_distances) {
82 assert(!is_8ply || !with_distances);
83
84 // Validate the file
85 if (!std::filesystem::exists(bookPath)) {
86 throw std::invalid_argument("Book file does not exist: " +
87 bookPath.string());
88 }
89
90#ifndef NDEBUG
91 // Infer database type from file size (if required)
92 const auto fileSize = std::filesystem::file_size(bookPath);
93#endif
94 if (is_8ply) {
95 assert(fileSize == SIZE_BYTES_8PLY_DB); // 8-ply with distances
96 } else if (with_distances) {
97 assert(fileSize ==
98 SIZE_BYTES_12PLY_DB_WITH_DIST); // 12-ply with distances
99 } else {
100 assert(fileSize == SIZE_BYTES_12PLY_DB); // 12-ply without distances
101 }
102
103 this->m_withDistances = with_distances;
104 this->m_is8ply = is_8ply;
105 this->m_book = readBook(bookPath, with_distances, is_8ply);
106 this->m_bookPath = bookPath;
107 this->m_nPly = (is_8ply ? 8 : 12);
108
109 assert(!with_distances || is_8ply ||
110 m_book.size() == SIZE_12PLY_DB_WITH_DIST); // 12-ply with distances
111
112 assert(with_distances || is_8ply ||
113 m_book.size() == SIZE_12PLY_DB); // 12-ply without distances
114
115 assert(!is_8ply ||
116 m_book.size() == SIZE_8PLY_DB); // 8-ply without distances
117 }
118
119 [[nodiscard]] auto getEntry(const size_t entryIdx) const {
120 return m_book.at(entryIdx);
121 }
122
123 [[nodiscard]] auto getBookSize() const { return m_book.size(); }
124
125 static std::tuple<key_t, int> readline(std::ifstream& file,
126 const bool with_distances,
127 const bool is_8ply) {
128 const decltype(file.gcount()) bytes_position = is_8ply ? 3 : 4;
129 char buffer[4] = {}; // Max buffer size for reading
130 file.read(buffer, bytes_position);
131
132 if (file.gcount() != bytes_position) {
133 // EOF or read error
134 return {0, 0};
135 }
136
137 // Convert the read bytes into an integer
138 key_t huffman_position = 0;
139 for (decltype(file.gcount()) i = 0; i < bytes_position; ++i) {
140 huffman_position =
141 (huffman_position << 8) | static_cast<unsigned char>(buffer[i]);
142 }
143
144 if (!is_8ply) {
145 // Handle signed interpretation for 4-byte numbers
146 if (huffman_position & (1LL << ((bytes_position * 8) - 1))) {
147 huffman_position -= (1LL << (bytes_position * 8));
148 }
149 }
150
151 value_t score = 0;
152 if (with_distances) {
153 // Read one additional byte for the score
154 char score_byte;
155 if (file.read(&score_byte, 1)) {
156 score = static_cast<int8_t>(score_byte);
157 } else {
158 // EOF after reading huffman_position
159 return {0, 0};
160 }
161 } else {
162 // Last 2 bits indicate the score
163 score = (static_cast<value_t>(huffman_position) & 3) * -1;
164 huffman_position = huffman_position & ~3;
165 }
166
167 return {huffman_position, score};
168 }
169
170 int getNPly() const { return m_nPly; }
171
172 static std::vector<std::tuple<key_t, value_t>> readBook(
173 const std::filesystem::path& filename, const bool with_distances = true,
174 const bool is_8ply = false) {
175 std::vector<std::tuple<key_t, value_t>> book; // To store the book entries
176 std::ifstream file(filename, std::ios::binary);
177 if (!file) {
178 std::cerr << "Failed to open file: " << filename.string() << '\n';
179 return book; // Return an empty book if the file can't be opened
180 }
181
182 while (true) {
183 auto [position, score] = readline(file, with_distances, is_8ply);
184 if (file.eof()) {
185 break; // End of file reached
186 }
187 book.emplace_back(position, score);
188 }
189
190 return book;
191 }
192
193 template <typename T>
194 static int sign(T value) {
195 return (value > 0) - (value < 0);
196 }
197
198 int inline convertValue(const int value, const Board& b) const {
199 if (!m_withDistances) return value;
200
201 // adjust value to our scoring system
202 int movesLeft = std::abs(value) - 100 + b.movesLeft();
203 return sign(value) * (movesLeft / 2 + 1);
204 }
205
206 [[nodiscard]] bool isInBook(const Board& b) const {
207 // Only check if exactly this position is in the book
208 return (binarySearch(b.toHuffman()) != NONE_VALUE);
209 }
210
211 [[nodiscard]] int getBoardValue(const Board& b) const {
212 if (!((m_is8ply && b.countTokens() == 8) || b.countTokens() == 12)) {
213 return NONE_VALUE;
214 }
215
216 // # first try this position
217 auto p = b.toHuffman();
218 int val = binarySearch(p);
219 if (val != NONE_VALUE) {
220 return convertValue(val, b);
221 }
222
223 // # Did not find position. Look for the mirrored equivalent
224 p = b.mirror().toHuffman();
225 val = binarySearch(p);
226 if (!m_withDistances && val == NONE_VALUE) {
227 // only for the 8-ply and 12-ply database without distances
228 val = 1; // if a position is not in the database, then this means that
229 // player 1 wins
230
231 // obsolete:
232 // Apparently, positions with 2 immediate threats for player Red are
233 // missing in the 8-ply database
234 // if (m_is8ply && !b.generateNonLosingMoves()) {
235 // val = -1;
236 //}
237 } else if (val == NONE_VALUE) {
238 // This is a special case. Positions, where player 1 (yellow) can
239 // immediately win, are not encoded in the databases.
240 return (b.movesLeft() + 1) / 2;
241 }
242 assert(val != NONE_VALUE);
243 return convertValue(val, b);
244 }
245};
246
247} // namespace BitBully
248
249#endif // OPENINGBOOK_H