BitBully 0.0.78
A fast, perfect-play Connect-4 engine in modern C++
Loading...
Searching...
No Matches
OpeningBook.h
Go to the documentation of this file.
1
15#ifndef OPENINGBOOK_H
16#define OPENINGBOOK_H
17
18#include <Board.h>
19
20#include <cassert>
21#include <filesystem>
22#include <fstream>
23#include <iostream>
24#include <limits>
25#include <tuple>
26#include <vector>
27
28namespace BitBully {
29
40// TODO: guess database type from size of file!
42 private:
44 using key_t = int;
46 using value_t = int8_t;
48 static constexpr size_t SIZE_BYTES_8PLY_DB = 103'545; // 102'858;
50 static constexpr size_t SIZE_BYTES_12PLY_DB = 6'943'780;
52 static constexpr size_t SIZE_BYTES_12PLY_DB_WITH_DIST = 21'004'495;
53
55 static constexpr size_t SIZE_8PLY_DB = 34'515;
57 static constexpr size_t SIZE_12PLY_DB = 1'735'945;
59 static constexpr size_t SIZE_12PLY_DB_WITH_DIST = 4'200'899;
60
61 std::vector<std::tuple<key_t, value_t>>
62 m_book;
63 bool m_withDistances{};
64 bool m_is8ply{};
65 std::filesystem::path m_bookPath;
66 int m_nPly{};
67
74 [[nodiscard]] value_t binarySearch(const key_t& huffmanCode) const {
75 // one could also use: std::lower_bound()
76 int l = 0; // dont use size_t to prevent undesired underflows
77 int r = m_book.size() - 1;
78 while (r >= l) {
79 const auto mid = (l + r + 1) / 2;
80 auto p = m_book.at(mid);
81 if (std::get<0>(p) == huffmanCode) {
82 return std::get<1>(p); // Found! return the value for this position
83 }
84 if (std::get<0>(p) > huffmanCode) {
85 r = mid - 1;
86 } else { // p < huffmanCode
87 l = mid + 1;
88 }
89 }
90
91 // Nothing found:
92 return std::numeric_limits<value_t>::min();
93 }
94
95 public:
97 static constexpr auto NONE_VALUE = std::numeric_limits<value_t>::min();
98
109 explicit OpeningBook(const std::filesystem::path& bookPath,
110 const bool is_8ply, const bool with_distances) {
111 init(bookPath, is_8ply, with_distances);
112 }
113
122 explicit OpeningBook(const std::filesystem::path& bookPath) {
123 if (!std::filesystem::exists(bookPath)) {
124 throw std::invalid_argument("Book file does not exist: " +
125 bookPath.string());
126 }
127
128 const auto fileSize = std::filesystem::file_size(bookPath);
129 // infer DB type from size:
130 const bool is8ply = (fileSize == SIZE_BYTES_8PLY_DB);
131 const bool withDistances = (fileSize == SIZE_BYTES_12PLY_DB_WITH_DIST);
132
133 init(bookPath, is8ply, withDistances);
134 }
135
137 auto getBook() const { return m_book; }
138
151 void init(const std::filesystem::path& bookPath, const bool is_8ply,
152 const bool with_distances) {
153 assert(!is_8ply || !with_distances);
154
155 // Validate the file
156 if (!std::filesystem::exists(bookPath)) {
157 throw std::invalid_argument("Book file does not exist: " +
158 bookPath.string());
159 }
160
161#ifndef NDEBUG
162 // Infer database type from file size (if required)
163 const auto fileSize = std::filesystem::file_size(bookPath);
164#endif
165 if (is_8ply) {
166 assert(fileSize == SIZE_BYTES_8PLY_DB); // 8-ply with distances
167 } else if (with_distances) {
168 assert(fileSize ==
169 SIZE_BYTES_12PLY_DB_WITH_DIST); // 12-ply with distances
170 } else {
171 assert(fileSize == SIZE_BYTES_12PLY_DB); // 12-ply without distances
172 }
173
174 this->m_withDistances = with_distances;
175 this->m_is8ply = is_8ply;
176 this->m_book = readBook(bookPath, with_distances, is_8ply);
177 this->m_bookPath = bookPath;
178 this->m_nPly = (is_8ply ? 8 : 12);
179
180 assert(!with_distances || is_8ply ||
181 m_book.size() == SIZE_12PLY_DB_WITH_DIST); // 12-ply with distances
182
183 assert(with_distances || is_8ply ||
184 m_book.size() == SIZE_12PLY_DB); // 12-ply without distances
185
186 assert(!is_8ply ||
187 m_book.size() == SIZE_8PLY_DB); // 8-ply without distances
188 }
189
196 [[nodiscard]] auto getEntry(const size_t entryIdx) const {
197 return m_book.at(entryIdx);
198 }
199
201 [[nodiscard]] auto getBookSize() const { return m_book.size(); }
202
216 static std::tuple<key_t, int> readline(std::ifstream& file,
217 const bool with_distances,
218 const bool is_8ply) {
219 const decltype(file.gcount()) bytes_position = is_8ply ? 3 : 4;
220 char buffer[4] = {}; // Max buffer size for reading
221 file.read(buffer, bytes_position);
222
223 if (file.gcount() != bytes_position) {
224 // EOF or read error
225 return {0, 0};
226 }
227
228 // Convert the read bytes into an integer
229 key_t huffman_position = 0;
230 for (decltype(file.gcount()) i = 0; i < bytes_position; ++i) {
231 huffman_position =
232 (huffman_position << 8) | static_cast<unsigned char>(buffer[i]);
233 }
234
235 if (!is_8ply) {
236 // Handle signed interpretation for 4-byte numbers
237 if (huffman_position & (1LL << ((bytes_position * 8) - 1))) {
238 huffman_position -= (1LL << (bytes_position * 8));
239 }
240 }
241
242 value_t score = 0;
243 if (with_distances) {
244 // Read one additional byte for the score
245 char score_byte;
246 if (file.read(&score_byte, 1)) {
247 score = static_cast<int8_t>(score_byte);
248 } else {
249 // EOF after reading huffman_position
250 return {0, 0};
251 }
252 } else {
253 // Last 2 bits indicate the score
254 score = (static_cast<value_t>(huffman_position) & 3) * -1;
255 huffman_position = huffman_position & ~3;
256 }
257
258 return {huffman_position, score};
259 }
260
262 int getNPly() const { return m_nPly; }
263
273 static std::vector<std::tuple<key_t, value_t>> readBook(
274 const std::filesystem::path& filename, const bool with_distances = true,
275 const bool is_8ply = false) {
276 std::vector<std::tuple<key_t, value_t>> book; // To store the book entries
277 std::ifstream file(filename, std::ios::binary);
278 if (!file) {
279 std::cerr << "Failed to open file: " << filename.string() << '\n';
280 return book; // Return an empty book if the file can't be opened
281 }
282
283 while (true) {
284 auto [position, score] = readline(file, with_distances, is_8ply);
285 if (file.eof()) {
286 break; // End of file reached
287 }
288 book.emplace_back(position, score);
289 }
290
291 return book;
292 }
293
300 template <typename T>
301 static int sign(T value) {
302 return (value > 0) - (value < 0);
303 }
304
317 int inline convertValue(const int value, const Board& b) const {
318 if (!m_withDistances) return value;
319
320 // adjust value to our scoring system
321 int movesLeft = std::abs(value) - 100 + b.movesLeft();
322 return sign(value) * (movesLeft / 2 + 1);
323 }
324
335 [[nodiscard]] bool isInBook(const Board& b) const {
336 return (binarySearch(b.toHuffman()) != NONE_VALUE);
337 }
338
352 [[nodiscard]] int getBoardValue(const Board& b) const {
353 if (!((m_is8ply && b.countTokens() == 8) || b.countTokens() == 12)) {
354 return NONE_VALUE;
355 }
356
357 // # first try this position
358 auto p = b.toHuffman();
359 int val = binarySearch(p);
360 if (val != NONE_VALUE) {
361 return convertValue(val, b);
362 }
363
364 // # Did not find position. Look for the mirrored equivalent
365 p = b.mirror().toHuffman();
366 val = binarySearch(p);
367 if (!m_withDistances && val == NONE_VALUE) {
368 // only for the 8-ply and 12-ply database without distances
369 val = 1; // if a position is not in the database, then this means that
370 // player 1 wins
371
372 // obsolete:
373 // Apparently, positions with 2 immediate threats for player Red are
374 // missing in the 8-ply database
375 // if (m_is8ply && !b.generateNonLosingMoves()) {
376 // val = -1;
377 //}
378 } else if (val == NONE_VALUE) {
379 // This is a special case. Positions, where player 1 (yellow) can
380 // immediately win, are not encoded in the databases.
381 return (b.movesLeft() + 1) / 2;
382 }
383 assert(val != NONE_VALUE);
384 return convertValue(val, b);
385 }
386};
387
388} // namespace BitBully
389
390#endif // OPENINGBOOK_H
Bitboard-based representation of a Connect-4 position.
Connect-4 position represented as a pair of 64-bit bitboards.
Definition Board.h:204
TMovesCounter countTokens() const
Number of stones currently placed on the board.
Definition Board.h:506
Board mirror() const
Mirror the board around its central column.
Definition Board.cpp:501
TMovesCounter movesLeft() const
Number of plies remaining until the board is full.
Definition Board.h:503
int toHuffman() const
Encode the position as a compact Huffman-like integer.
Definition Board.h:650
int getNPly() const
Number of stones-on-board the book covers (8 or 12).
static std::tuple< key_t, int > readline(std::ifstream &file, const bool with_distances, const bool is_8ply)
Read a single entry from a binary book stream.
static std::vector< std::tuple< key_t, value_t > > readBook(const std::filesystem::path &filename, const bool with_distances=true, const bool is_8ply=false)
Slurp an entire opening book file into memory.
auto getBook() const
Copy of the underlying sorted (key, value) array.
auto getBookSize() const
Number of entries currently held in memory.
int convertValue(const int value, const Board &b) const
Translate a raw book value into the engine's score convention.
OpeningBook(const std::filesystem::path &bookPath)
Load an opening book and auto-detect its flavour.
auto getEntry(const size_t entryIdx) const
Random-access getter for raw book entries.
static constexpr auto NONE_VALUE
Sentinel returned when a position is not present in the book.
Definition OpeningBook.h:97
static int sign(T value)
Numerical sign function returning -1, 0 or +1.
void init(const std::filesystem::path &bookPath, const bool is_8ply, const bool with_distances)
Re-initialise the book in place.
bool isInBook(const Board &b) const
Test whether the exact (non-mirrored) position is in the book.
int getBoardValue(const Board &b) const
Retrieve the engine score for a position covered by the book.
OpeningBook(const std::filesystem::path &bookPath, const bool is_8ply, const bool with_distances)
Load an opening book with explicit flavour selection.
Top-level namespace for the BitBully Connect-4 engine.
Definition BitBully.h:26