Commit ·
2cf2375
0
Parent(s):
initial
Browse files- .gitattributes +4 -0
- .gitignore +15 -0
- BestemsheCore.h +74 -0
- Compressor.cpp +174 -0
- Compressor.h +22 -0
- DATASET_CARD.md +273 -0
- DEPLOY.md +84 -0
- Dockerfile +27 -0
- Inference.h +174 -0
- Makefile +56 -0
- Oracle.h +127 -0
- README.md +56 -0
- Solver.cpp +349 -0
- Solver.h +92 -0
- StateIndex.h +107 -0
- app.py +750 -0
- commands.txt +57 -0
- i18n.py +361 -0
- main.cpp +242 -0
- oracle_bridge.cpp +69 -0
- packages.txt +3 -0
- query.cpp +138 -0
- requirements.txt +3 -0
- setup.py +36 -0
- upload_tablebase.py +66 -0
.gitattributes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LFS tracking for the tablebase (takes effect in the Hugging Face Space repo,
|
| 2 |
+
# where layers/ is committed; the GitHub repo keeps layers/ gitignored).
|
| 3 |
+
layers/compressed/*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
layers/compressed/compression_map.txt filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
.claude/
|
| 3 |
+
docs/
|
| 4 |
+
layers/
|
| 5 |
+
error.txt
|
| 6 |
+
*.o
|
| 7 |
+
*.so
|
| 8 |
+
build/
|
| 9 |
+
__pycache__/
|
| 10 |
+
bestemshe
|
| 11 |
+
query
|
| 12 |
+
*.dylib
|
| 13 |
+
debug_state
|
| 14 |
+
New/build/
|
| 15 |
+
New/
|
BestemsheCore.h
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include <cstdint>
|
| 3 |
+
#include <vector>
|
| 4 |
+
#include <algorithm>
|
| 5 |
+
#include <iostream>
|
| 6 |
+
|
| 7 |
+
namespace Bestemshe {
|
| 8 |
+
|
| 9 |
+
struct State {
|
| 10 |
+
uint8_t M; // Total stones captured (K_self + K_opp)
|
| 11 |
+
uint8_t K_self; // Active player's Kazan
|
| 12 |
+
uint8_t K_opp; // Opponent's Kazan
|
| 13 |
+
uint8_t board[10]; // Pits 0-4 (Self), 5-9 (Opponent)
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// Sows from pit `i` (0..4) and returns if a capture occurred.
|
| 17 |
+
// If valid, writes results to `next_s`.
|
| 18 |
+
inline bool ExecuteMoveAndFlip(const State& s, int i, State& flipped_out, bool& empties_opponent) {
|
| 19 |
+
if (s.board[i] == 0) return false;
|
| 20 |
+
|
| 21 |
+
State next_s = s;
|
| 22 |
+
int pieces = next_s.board[i];
|
| 23 |
+
next_s.board[i] = 0;
|
| 24 |
+
|
| 25 |
+
int current_pit = i;
|
| 26 |
+
if (pieces == 1) {
|
| 27 |
+
current_pit++;
|
| 28 |
+
if (current_pit==10) current_pit = 0;
|
| 29 |
+
next_s.board[current_pit]++;
|
| 30 |
+
} else {
|
| 31 |
+
next_s.board[i] = 1;
|
| 32 |
+
pieces--;
|
| 33 |
+
while (pieces > 0) {
|
| 34 |
+
current_pit++;
|
| 35 |
+
if (current_pit==10) current_pit = 0;
|
| 36 |
+
next_s.board[current_pit]++;
|
| 37 |
+
pieces--;
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
bool is_capture = false;
|
| 42 |
+
// Even Parity Capture on opponent's side (pits 5..9)
|
| 43 |
+
if (current_pit >= 5 && current_pit <= 9) {
|
| 44 |
+
if ((next_s.board[current_pit] & 1) == 0) {
|
| 45 |
+
uint8_t captured = next_s.board[current_pit];
|
| 46 |
+
next_s.K_self += captured;
|
| 47 |
+
next_s.M += captured;
|
| 48 |
+
next_s.board[current_pit] = 0;
|
| 49 |
+
is_capture = true;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Evaluate if opponent is completely emptied
|
| 54 |
+
empties_opponent = true;
|
| 55 |
+
for (int p = 5; p <= 9; ++p) {
|
| 56 |
+
if (next_s.board[p] > 0) {
|
| 57 |
+
empties_opponent = false;
|
| 58 |
+
break;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Flip board for opponent's turn perspective
|
| 63 |
+
flipped_out.M = next_s.M;
|
| 64 |
+
flipped_out.K_self = next_s.K_opp;
|
| 65 |
+
flipped_out.K_opp = next_s.K_self;
|
| 66 |
+
for (int p = 0; p < 5; ++p) {
|
| 67 |
+
flipped_out.board[p] = next_s.board[p + 5];
|
| 68 |
+
flipped_out.board[p + 5] = next_s.board[p];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return is_capture;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
} // namespace Bestemshe
|
Compressor.cpp
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "Compressor.h"
|
| 2 |
+
#include <fstream>
|
| 3 |
+
#include <iostream>
|
| 4 |
+
#include <limits>
|
| 5 |
+
#include <zstd.h> // Link -lzstd
|
| 6 |
+
|
| 7 |
+
namespace Bestemshe {
|
| 8 |
+
|
| 9 |
+
// -----------------------------------------------------------------------------
|
| 10 |
+
// Core Compression Hardware
|
| 11 |
+
// -----------------------------------------------------------------------------
|
| 12 |
+
static std::vector<uint8_t> CompressBlockZSTD(const uint8_t* src, size_t src_size) {
|
| 13 |
+
size_t bound = ZSTD_compressBound(src_size);
|
| 14 |
+
std::vector<uint8_t> comp_buf(bound);
|
| 15 |
+
|
| 16 |
+
// Level 19 is maximum compression. Decompression speed remains O(1) regarding level.
|
| 17 |
+
size_t comp_size = ZSTD_compress(
|
| 18 |
+
comp_buf.data(), bound,
|
| 19 |
+
src, src_size,
|
| 20 |
+
19
|
| 21 |
+
);
|
| 22 |
+
|
| 23 |
+
if (ZSTD_isError(comp_size)) {
|
| 24 |
+
std::cerr << "[FATAL] ZSTD Compression failed: " << ZSTD_getErrorName(comp_size) << std::endl;
|
| 25 |
+
return {};
|
| 26 |
+
}
|
| 27 |
+
comp_buf.resize(comp_size);
|
| 28 |
+
return comp_buf;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
static bool DecompressBlockZSTD(const uint8_t* src, size_t src_size, uint8_t* dest, size_t dest_size) {
|
| 32 |
+
size_t d_size = ZSTD_decompress(dest, dest_size, src, src_size);
|
| 33 |
+
if (ZSTD_isError(d_size)) {
|
| 34 |
+
std::cerr << "[FATAL] ZSTD Decompression failed: " << ZSTD_getErrorName(d_size) << std::endl;
|
| 35 |
+
return false;
|
| 36 |
+
}
|
| 37 |
+
return true;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// -----------------------------------------------------------------------------
|
| 41 |
+
// Blocked I/O Pipeline
|
| 42 |
+
// -----------------------------------------------------------------------------
|
| 43 |
+
void Compressor::CompressMicroLayer(const std::string& input_raw_path,
|
| 44 |
+
const std::string& output_bin_path,
|
| 45 |
+
size_t block_size) {
|
| 46 |
+
std::ifstream in(input_raw_path, std::ios::binary | std::ios::ate);
|
| 47 |
+
if (!in.is_open()) {
|
| 48 |
+
std::cerr << "[ERROR] Could not open raw input: " << input_raw_path << "\n";
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
size_t total_size = in.tellg();
|
| 53 |
+
in.seekg(0, std::ios::beg);
|
| 54 |
+
std::vector<uint8_t> raw_data(total_size);
|
| 55 |
+
in.read(reinterpret_cast<char*>(raw_data.data()), total_size);
|
| 56 |
+
in.close();
|
| 57 |
+
|
| 58 |
+
size_t bytes_per_block = block_size / 8;
|
| 59 |
+
if (bytes_per_block == 0) {
|
| 60 |
+
std::cerr << "[FATAL] block_size " << block_size << " too small (< 8 bits).\n";
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
size_t num_blocks = (raw_data.size() + bytes_per_block - 1) / bytes_per_block;
|
| 64 |
+
|
| 65 |
+
std::vector<std::vector<uint8_t>> compressed_blocks(num_blocks);
|
| 66 |
+
std::vector<uint32_t> block_offsets(num_blocks + 1, 0);
|
| 67 |
+
|
| 68 |
+
for (size_t b = 0; b < num_blocks; ++b) {
|
| 69 |
+
size_t start_idx = b * bytes_per_block;
|
| 70 |
+
size_t size = std::min(bytes_per_block, raw_data.size() - start_idx);
|
| 71 |
+
|
| 72 |
+
std::vector<uint8_t> block_tmp(bytes_per_block, 0); // zero padded for alignment
|
| 73 |
+
std::copy(raw_data.begin() + start_idx, raw_data.begin() + start_idx + size, block_tmp.begin());
|
| 74 |
+
|
| 75 |
+
compressed_blocks[b] = CompressBlockZSTD(block_tmp.data(), bytes_per_block);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Write architecture: [Num Blocks] [Offsets Header Table] [Compressed Payload]
|
| 79 |
+
std::ofstream out(output_bin_path, std::ios::binary);
|
| 80 |
+
uint32_t num_blocks_u32 = static_cast<uint32_t>(num_blocks);
|
| 81 |
+
out.write(reinterpret_cast<const char*>(&num_blocks_u32), sizeof(uint32_t));
|
| 82 |
+
|
| 83 |
+
uint32_t header_bytes_size = sizeof(uint32_t) + (num_blocks_u32 + 1) * sizeof(uint32_t);
|
| 84 |
+
uint64_t current_offset = header_bytes_size;
|
| 85 |
+
|
| 86 |
+
for (size_t b = 0; b < num_blocks; ++b) {
|
| 87 |
+
block_offsets[b] = static_cast<uint32_t>(current_offset);
|
| 88 |
+
current_offset += compressed_blocks[b].size();
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Bounds check to guarantee file fits in uint32 offset layout
|
| 92 |
+
if (current_offset > std::numeric_limits<uint32_t>::max()) {
|
| 93 |
+
std::cerr << "[FATAL] Compressed file exceeds 4GB uint32 offset limit: " << output_bin_path << "\n";
|
| 94 |
+
out.close();
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
block_offsets[num_blocks] = static_cast<uint32_t>(current_offset);
|
| 98 |
+
|
| 99 |
+
out.write(reinterpret_cast<const char*>(block_offsets.data()), block_offsets.size() * sizeof(uint32_t));
|
| 100 |
+
for (size_t b = 0; b < num_blocks; ++b) {
|
| 101 |
+
out.write(reinterpret_cast<const char*>(compressed_blocks[b].data()), compressed_blocks[b].size());
|
| 102 |
+
}
|
| 103 |
+
out.close();
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
std::vector<uint8_t> Compressor::DecompressMicroLayer(const std::string& input_bin_path,
|
| 107 |
+
size_t bytes_per_block,
|
| 108 |
+
size_t expected_raw_size) {
|
| 109 |
+
std::ifstream in(input_bin_path, std::ios::binary);
|
| 110 |
+
if (!in.is_open()) {
|
| 111 |
+
std::cerr << "[ERROR] Could not open compressed input: " << input_bin_path << "\n";
|
| 112 |
+
return {};
|
| 113 |
+
}
|
| 114 |
+
if (bytes_per_block == 0) {
|
| 115 |
+
std::cerr << "[FATAL] bytes_per_block == 0\n";
|
| 116 |
+
return {};
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
uint32_t num_blocks = 0;
|
| 120 |
+
in.read(reinterpret_cast<char*>(&num_blocks), sizeof(uint32_t));
|
| 121 |
+
if (!in) return {};
|
| 122 |
+
|
| 123 |
+
// Prevent malicious or corrupt header OOM attacks
|
| 124 |
+
size_t max_plausible_blocks = (expected_raw_size / bytes_per_block) + 2;
|
| 125 |
+
if (num_blocks == 0 || num_blocks > max_plausible_blocks) {
|
| 126 |
+
std::cerr << "[FATAL] Implausible block count " << num_blocks << " in " << input_bin_path << "\n";
|
| 127 |
+
return {};
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
std::vector<uint32_t> block_offsets(num_blocks + 1);
|
| 131 |
+
in.read(reinterpret_cast<char*>(block_offsets.data()), (num_blocks + 1) * sizeof(uint32_t));
|
| 132 |
+
if (!in) return {};
|
| 133 |
+
|
| 134 |
+
uint32_t header_size = sizeof(uint32_t) + (num_blocks + 1) * sizeof(uint32_t);
|
| 135 |
+
uint32_t total_size = block_offsets.back();
|
| 136 |
+
|
| 137 |
+
if (total_size < header_size || block_offsets[0] != header_size) {
|
| 138 |
+
std::cerr << "[FATAL] Corrupt offset table in " << input_bin_path << "\n";
|
| 139 |
+
return {};
|
| 140 |
+
}
|
| 141 |
+
for (size_t b = 0; b < num_blocks; ++b) {
|
| 142 |
+
if (block_offsets[b + 1] < block_offsets[b]) {
|
| 143 |
+
std::cerr << "[FATAL] Non-monotonic offsets in " << input_bin_path << "\n";
|
| 144 |
+
return {};
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
std::vector<uint8_t> compressed_blob(total_size - header_size);
|
| 149 |
+
in.read(reinterpret_cast<char*>(compressed_blob.data()), compressed_blob.size());
|
| 150 |
+
if (!in) return {};
|
| 151 |
+
|
| 152 |
+
std::vector<uint8_t> raw_out;
|
| 153 |
+
raw_out.reserve(static_cast<size_t>(num_blocks) * bytes_per_block);
|
| 154 |
+
|
| 155 |
+
for (size_t b = 0; b < num_blocks; ++b) {
|
| 156 |
+
uint32_t start = block_offsets[b] - header_size;
|
| 157 |
+
uint32_t end = block_offsets[b + 1] - header_size;
|
| 158 |
+
uint32_t comp_size = end - start;
|
| 159 |
+
|
| 160 |
+
std::vector<uint8_t> block_raw(bytes_per_block, 0);
|
| 161 |
+
const uint8_t* src = compressed_blob.data() + start;
|
| 162 |
+
|
| 163 |
+
if (!DecompressBlockZSTD(src, comp_size, block_raw.data(), block_raw.size())) {
|
| 164 |
+
std::cerr << "[FATAL] Block " << b << " decompression failed in " << input_bin_path << "\n";
|
| 165 |
+
return {};
|
| 166 |
+
}
|
| 167 |
+
raw_out.insert(raw_out.end(), block_raw.begin(), block_raw.end());
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if (raw_out.size() > expected_raw_size) raw_out.resize(expected_raw_size);
|
| 171 |
+
return raw_out;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
} // namespace Bestemshe
|
Compressor.h
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include <string>
|
| 3 |
+
#include <vector>
|
| 4 |
+
#include <cstdint>
|
| 5 |
+
#include <cstddef>
|
| 6 |
+
|
| 7 |
+
namespace Bestemshe {
|
| 8 |
+
|
| 9 |
+
class Compressor {
|
| 10 |
+
public:
|
| 11 |
+
// Exclusively utilizes ZSTD (Level 19).
|
| 12 |
+
// Default block size: 33,554,432 bits (4MB packed bytes).
|
| 13 |
+
static void CompressMicroLayer(const std::string& input_raw_path,
|
| 14 |
+
const std::string& output_bin_path,
|
| 15 |
+
size_t block_size = 33554432);
|
| 16 |
+
|
| 17 |
+
static std::vector<uint8_t> DecompressMicroLayer(const std::string& input_bin_path,
|
| 18 |
+
size_t bytes_per_block,
|
| 19 |
+
size_t expected_raw_size);
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
} // namespace Bestemshe
|
DATASET_CARD.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
task_categories:
|
| 4 |
+
- reinforcement-learning
|
| 5 |
+
language:
|
| 6 |
+
- en
|
| 7 |
+
tags:
|
| 8 |
+
- Bestemshe
|
| 9 |
+
- Togyzkumalak
|
| 10 |
+
- Togyzqumalaq
|
| 11 |
+
- Mancala
|
| 12 |
+
- Game-Theory
|
| 13 |
+
- Retrograde-Analysis
|
| 14 |
+
- Tablebase
|
| 15 |
+
- Strong-Solution
|
| 16 |
+
pretty_name: Bestemshe TableBase — Strong Solution Proof
|
| 17 |
+
size_categories:
|
| 18 |
+
- 10B<n<100B
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
# Bestemshe Table Base — Strong Solution Proof
|
| 22 |
+
|
| 23 |
+
**Authors:** Ansar Zeinulla & Murat Manassov
|
| 24 |
+
**Affiliation:** Nazarbayev University, Kazakhstan
|
| 25 |
+
**Code Repository:** [github.com/ansarzeinulla/Bestemshe](https://github.com/ansarzeinulla/Bestemshe)
|
| 26 |
+
**Live Interactive Explorer:** [huggingface.co/spaces/ansarzeinulla/Bestemshe-God-Algorithm](https://huggingface.co/spaces/ansarzeinulla/bestemshe-god-algorithm)
|
| 27 |
+
|
| 28 |
+
Bestemshe is a traditional Kazakh two-player mancala-style game (5 pits per player, 50 total stones). This artifact is a **strong solution** of the game: for every reachable, legal position the exact game-theoretic value (win / draw for the side to move) has been computed by retrograde analysis and stored, enabling perfect play via direct $O(1)$ memory-mapped lookup without runtime search.
|
| 29 |
+
|
| 30 |
+
Together with the solver that produced it, this tablebase constitutes a machine-verifiable proof of the game's outcome under optimal play: **a forced win for the second player (Follower)**.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Game Encoding
|
| 35 |
+
|
| 36 |
+
A position is described by the two Kazans (captured-stone stores) `K1` (side to move) and `K2` (opponent), and ten pits of `0..N` stones. The 50 stones are conserved:
|
| 37 |
+
$$\sum_{i=1}^{10} \text{pits}[i] + K_1 + K_2 = 50$$
|
| 38 |
+
|
| 39 |
+
Captures are strictly even, so each Kazan score is an even integer. A Kazan reaching $\ge 26$ decides the game immediately and lies outside the stored range ($0 \le K_1, K_2 \le 24$). Positions are stored canonically from the **side-to-move** perspective.
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Layer Layout
|
| 44 |
+
|
| 45 |
+
The tablebase is sharded into **layers indexed by the Kazan pair `(K1, K2)`**, where each Kazan is even in $0..24$ ($13 \times 13 = 169$ pairs). Every pair consists of two Zstandard-compressed bitset files:
|
| 46 |
+
|
| 47 |
+
- `layer_<K1>_<K2>_win.bin.zst` — The WIN/LOSS bitset for that layer.
|
| 48 |
+
- `layer_<K1>_<K2>_draw.bin.zst` — The DRAW bitset for that layer.
|
| 49 |
+
|
| 50 |
+
Files are Zstandard-compressed bitsets indexed by a combinatorial ranking of the pit configuration (`StateIndex`).
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## Summary Statistics
|
| 55 |
+
|
| 56 |
+
| Metric | Value |
|
| 57 |
+
| :--- | :--- |
|
| 58 |
+
| **Layer pairs `(K1, K2)`** | 169 |
|
| 59 |
+
| **Total files** | 338 (169 win + 169 draw) |
|
| 60 |
+
| **WIN files total size** | 6.0 GB |
|
| 61 |
+
| **DRAW files total size** | 2.4 GB |
|
| 62 |
+
| **Grand Total Size** | **8.3 GB** (8,962,782,421 bytes) |
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## How to Query (Quickstart)
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
import zstandard as zstd
|
| 70 |
+
|
| 71 |
+
# Example: Loading a specific layer in Python
|
| 72 |
+
def load_layer(k1, k2, result_type="win"):
|
| 73 |
+
filename = f"data/layer_{k1}_{k2}_{result_type}.bin.zst"
|
| 74 |
+
with open(filename, 'rb') as fh:
|
| 75 |
+
dctx = zstd.ZstdDecompressor()
|
| 76 |
+
decompressed_data = dctx.decompress(fh.read())
|
| 77 |
+
return decompressed_data
|
| 78 |
+
|
| 79 |
+
# Query bit at index
|
| 80 |
+
def is_winning_state(decompressed_bytes, state_index):
|
| 81 |
+
byte_idx = state_index // 8
|
| 82 |
+
bit_idx = state_index % 8
|
| 83 |
+
return bool((decompressed_bytes[byte_idx] >> bit_idx) & 1)
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## Per-layer file sizes
|
| 87 |
+
|
| 88 |
+
| K1 | K2 | win.bin | draw.bin |
|
| 89 |
+
| ---: | ---: | ---: | ---: |
|
| 90 |
+
| 0 | 0 | 960.4 MB | 394.1 MB |
|
| 91 |
+
| 0 | 2 | 755.2 MB | 287.9 MB |
|
| 92 |
+
| 0 | 4 | 456.0 MB | 164.6 MB |
|
| 93 |
+
| 0 | 6 | 238.7 MB | 87.6 MB |
|
| 94 |
+
| 0 | 8 | 114.8 MB | 44.0 MB |
|
| 95 |
+
| 0 | 10 | 52.5 MB | 20.3 MB |
|
| 96 |
+
| 0 | 12 | 23.8 MB | 8.3 MB |
|
| 97 |
+
| 0 | 14 | 11.6 MB | 3.1 MB |
|
| 98 |
+
| 0 | 16 | 6.3 MB | 978.9 KB |
|
| 99 |
+
| 0 | 18 | 3.5 MB | 252.9 KB |
|
| 100 |
+
| 0 | 20 | 1.9 MB | 64.5 KB |
|
| 101 |
+
| 0 | 22 | 828.4 KB | 11.0 KB |
|
| 102 |
+
| 0 | 24 | 202.5 KB | 455.0 B |
|
| 103 |
+
| 2 | 0 | 440.9 MB | 188.7 MB |
|
| 104 |
+
| 2 | 2 | 487.8 MB | 204.0 MB |
|
| 105 |
+
| 2 | 4 | 367.9 MB | 143.3 MB |
|
| 106 |
+
| 2 | 6 | 212.7 MB | 79.0 MB |
|
| 107 |
+
| 2 | 8 | 106.5 MB | 39.8 MB |
|
| 108 |
+
| 2 | 10 | 49.4 MB | 18.6 MB |
|
| 109 |
+
| 2 | 12 | 22.4 MB | 7.8 MB |
|
| 110 |
+
| 2 | 14 | 10.8 MB | 2.9 MB |
|
| 111 |
+
| 2 | 16 | 5.7 MB | 922.9 KB |
|
| 112 |
+
| 2 | 18 | 3.1 MB | 236.7 KB |
|
| 113 |
+
| 2 | 20 | 1.6 MB | 59.2 KB |
|
| 114 |
+
| 2 | 22 | 702.7 KB | 10.5 KB |
|
| 115 |
+
| 2 | 24 | 160.6 KB | 306.0 B |
|
| 116 |
+
| 4 | 0 | 149.2 MB | 63.0 MB |
|
| 117 |
+
| 4 | 2 | 220.2 MB | 96.1 MB |
|
| 118 |
+
| 4 | 4 | 233.8 MB | 99.9 MB |
|
| 119 |
+
| 4 | 6 | 168.5 MB | 67.0 MB |
|
| 120 |
+
| 4 | 8 | 93.1 MB | 35.0 MB |
|
| 121 |
+
| 4 | 10 | 44.9 MB | 16.5 MB |
|
| 122 |
+
| 4 | 12 | 20.6 MB | 7.0 MB |
|
| 123 |
+
| 4 | 14 | 9.9 MB | 2.7 MB |
|
| 124 |
+
| 4 | 16 | 5.1 MB | 856.7 KB |
|
| 125 |
+
| 4 | 18 | 2.7 MB | 217.5 KB |
|
| 126 |
+
| 4 | 20 | 1.4 MB | 53.2 KB |
|
| 127 |
+
| 4 | 22 | 561.6 KB | 9.3 KB |
|
| 128 |
+
| 4 | 24 | 123.9 KB | 157.0 B |
|
| 129 |
+
| 6 | 0 | 43.6 MB | 17.2 MB |
|
| 130 |
+
| 6 | 2 | 72.5 MB | 31.4 MB |
|
| 131 |
+
| 6 | 4 | 103.6 MB | 45.9 MB |
|
| 132 |
+
| 6 | 6 | 104.9 MB | 45.7 MB |
|
| 133 |
+
| 6 | 8 | 71.8 MB | 28.9 MB |
|
| 134 |
+
| 6 | 10 | 38.1 MB | 14.1 MB |
|
| 135 |
+
| 6 | 12 | 18.1 MB | 6.1 MB |
|
| 136 |
+
| 6 | 14 | 8.7 MB | 2.4 MB |
|
| 137 |
+
| 6 | 16 | 4.4 MB | 756.5 KB |
|
| 138 |
+
| 6 | 18 | 2.3 MB | 195.4 KB |
|
| 139 |
+
| 6 | 20 | 1.1 MB | 46.3 KB |
|
| 140 |
+
| 6 | 22 | 426.8 KB | 7.5 KB |
|
| 141 |
+
| 6 | 24 | 91.1 KB | 157.0 B |
|
| 142 |
+
| 8 | 0 | 12.7 MB | 4.5 MB |
|
| 143 |
+
| 8 | 2 | 20.5 MB | 8.2 MB |
|
| 144 |
+
| 8 | 4 | 33.4 MB | 14.6 MB |
|
| 145 |
+
| 8 | 6 | 45.6 MB | 20.4 MB |
|
| 146 |
+
| 8 | 8 | 43.5 MB | 19.2 MB |
|
| 147 |
+
| 8 | 10 | 28.2 MB | 11.3 MB |
|
| 148 |
+
| 8 | 12 | 14.5 MB | 5.1 MB |
|
| 149 |
+
| 8 | 14 | 7.1 MB | 2.0 MB |
|
| 150 |
+
| 8 | 16 | 3.6 MB | 650.7 KB |
|
| 151 |
+
| 8 | 18 | 1.8 MB | 168.6 KB |
|
| 152 |
+
| 8 | 20 | 855.6 KB | 38.3 KB |
|
| 153 |
+
| 8 | 22 | 306.1 KB | 6.5 KB |
|
| 154 |
+
| 8 | 24 | 66.4 KB | 157.0 B |
|
| 155 |
+
| 10 | 0 | 4.3 MB | 1.3 MB |
|
| 156 |
+
| 10 | 2 | 6.2 MB | 2.1 MB |
|
| 157 |
+
| 10 | 4 | 9.6 MB | 3.8 MB |
|
| 158 |
+
| 10 | 6 | 14.9 MB | 6.3 MB |
|
| 159 |
+
| 10 | 8 | 18.8 MB | 8.2 MB |
|
| 160 |
+
| 10 | 10 | 16.5 MB | 7.3 MB |
|
| 161 |
+
| 10 | 12 | 10.0 MB | 3.9 MB |
|
| 162 |
+
| 10 | 14 | 5.1 MB | 1.5 MB |
|
| 163 |
+
| 10 | 16 | 2.6 MB | 521.9 KB |
|
| 164 |
+
| 10 | 18 | 1.3 MB | 131.7 KB |
|
| 165 |
+
| 10 | 20 | 571.3 KB | 28.1 KB |
|
| 166 |
+
| 10 | 22 | 199.9 KB | 4.9 KB |
|
| 167 |
+
| 10 | 24 | 42.6 KB | 157.0 B |
|
| 168 |
+
| 12 | 0 | 2.0 MB | 371.6 KB |
|
| 169 |
+
| 12 | 2 | 2.4 MB | 596.9 KB |
|
| 170 |
+
| 12 | 4 | 3.3 MB | 990.2 KB |
|
| 171 |
+
| 12 | 6 | 4.8 MB | 1.7 MB |
|
| 172 |
+
| 12 | 8 | 6.7 MB | 2.6 MB |
|
| 173 |
+
| 12 | 10 | 7.3 MB | 2.9 MB |
|
| 174 |
+
| 12 | 12 | 5.5 MB | 2.2 MB |
|
| 175 |
+
| 12 | 14 | 3.1 MB | 1.0 MB |
|
| 176 |
+
| 12 | 16 | 1.6 MB | 334.4 KB |
|
| 177 |
+
| 12 | 18 | 752.9 KB | 79.0 KB |
|
| 178 |
+
| 12 | 20 | 316.2 KB | 13.6 KB |
|
| 179 |
+
| 12 | 22 | 108.6 KB | 3.0 KB |
|
| 180 |
+
| 12 | 24 | 25.5 KB | 157.0 B |
|
| 181 |
+
| 14 | 0 | 1.3 MB | 107.1 KB |
|
| 182 |
+
| 14 | 2 | 1.3 MB | 176.7 KB |
|
| 183 |
+
| 14 | 4 | 1.5 MB | 287.4 KB |
|
| 184 |
+
| 14 | 6 | 2.0 MB | 481.2 KB |
|
| 185 |
+
| 14 | 8 | 2.6 MB | 770.0 KB |
|
| 186 |
+
| 14 | 10 | 3.0 MB | 983.1 KB |
|
| 187 |
+
| 14 | 12 | 2.6 MB | 878.9 KB |
|
| 188 |
+
| 14 | 14 | 1.5 MB | 481.3 KB |
|
| 189 |
+
| 14 | 16 | 787.7 KB | 162.0 KB |
|
| 190 |
+
| 14 | 18 | 356.4 KB | 37.1 KB |
|
| 191 |
+
| 14 | 20 | 144.9 KB | 3.4 KB |
|
| 192 |
+
| 14 | 22 | 51.7 KB | 157.0 B |
|
| 193 |
+
| 14 | 24 | 12.1 KB | 157.0 B |
|
| 194 |
+
| 16 | 0 | 919.0 KB | 30.7 KB |
|
| 195 |
+
| 16 | 2 | 903.3 KB | 47.6 KB |
|
| 196 |
+
| 16 | 4 | 933.4 KB | 77.1 KB |
|
| 197 |
+
| 16 | 6 | 1.0 MB | 127.0 KB |
|
| 198 |
+
| 16 | 8 | 1.2 MB | 211.7 KB |
|
| 199 |
+
| 16 | 10 | 1.4 MB | 286.9 KB |
|
| 200 |
+
| 16 | 12 | 1.2 MB | 258.7 KB |
|
| 201 |
+
| 16 | 14 | 727.8 KB | 149.3 KB |
|
| 202 |
+
| 16 | 16 | 344.9 KB | 58.0 KB |
|
| 203 |
+
| 16 | 18 | 145.1 KB | 12.9 KB |
|
| 204 |
+
| 16 | 20 | 56.2 KB | 157.0 B |
|
| 205 |
+
| 16 | 22 | 19.6 KB | 157.0 B |
|
| 206 |
+
| 16 | 24 | 5.3 KB | 157.0 B |
|
| 207 |
+
| 18 | 0 | 663.0 KB | 6.8 KB |
|
| 208 |
+
| 18 | 2 | 620.0 KB | 11.2 KB |
|
| 209 |
+
| 18 | 4 | 595.9 KB | 18.1 KB |
|
| 210 |
+
| 18 | 6 | 609.5 KB | 32.1 KB |
|
| 211 |
+
| 18 | 8 | 672.2 KB | 53.9 KB |
|
| 212 |
+
| 18 | 10 | 697.9 KB | 76.3 KB |
|
| 213 |
+
| 18 | 12 | 555.7 KB | 64.2 KB |
|
| 214 |
+
| 18 | 14 | 316.9 KB | 32.5 KB |
|
| 215 |
+
| 18 | 16 | 140.8 KB | 12.9 KB |
|
| 216 |
+
| 18 | 18 | 55.5 KB | 157.0 B |
|
| 217 |
+
| 18 | 20 | 19.3 KB | 157.0 B |
|
| 218 |
+
| 18 | 22 | 6.3 KB | 157.0 B |
|
| 219 |
+
| 18 | 24 | 1.8 KB | 157.0 B |
|
| 220 |
+
| 20 | 0 | 457.5 KB | 2.5 KB |
|
| 221 |
+
| 20 | 2 | 407.7 KB | 3.0 KB |
|
| 222 |
+
| 20 | 4 | 371.0 KB | 5.8 KB |
|
| 223 |
+
| 20 | 6 | 354.0 KB | 11.2 KB |
|
| 224 |
+
| 20 | 8 | 349.6 KB | 18.7 KB |
|
| 225 |
+
| 20 | 10 | 318.2 KB | 21.5 KB |
|
| 226 |
+
| 20 | 12 | 226.6 KB | 13.3 KB |
|
| 227 |
+
| 20 | 14 | 122.3 KB | 3.5 KB |
|
| 228 |
+
| 20 | 16 | 53.4 KB | 157.0 B |
|
| 229 |
+
| 20 | 18 | 19.4 KB | 157.0 B |
|
| 230 |
+
| 20 | 20 | 6.1 KB | 157.0 B |
|
| 231 |
+
| 20 | 22 | 1.8 KB | 157.0 B |
|
| 232 |
+
| 20 | 24 | 609.0 B | 157.0 B |
|
| 233 |
+
| 22 | 0 | 255.1 KB | 720.0 B |
|
| 234 |
+
| 22 | 2 | 217.5 KB | 1.0 KB |
|
| 235 |
+
| 22 | 4 | 186.4 KB | 1.7 KB |
|
| 236 |
+
| 22 | 6 | 163.2 KB | 2.1 KB |
|
| 237 |
+
| 22 | 8 | 142.2 KB | 3.4 KB |
|
| 238 |
+
| 22 | 10 | 113.0 KB | 3.6 KB |
|
| 239 |
+
| 22 | 12 | 73.0 KB | 2.7 KB |
|
| 240 |
+
| 22 | 14 | 39.9 KB | 157.0 B |
|
| 241 |
+
| 22 | 16 | 17.6 KB | 157.0 B |
|
| 242 |
+
| 22 | 18 | 6.3 KB | 157.0 B |
|
| 243 |
+
| 22 | 20 | 1.9 KB | 157.0 B |
|
| 244 |
+
| 22 | 22 | 598.0 B | 157.0 B |
|
| 245 |
+
| 22 | 24 | 240.0 B | 157.0 B |
|
| 246 |
+
| 24 | 0 | 88.0 KB | 455.0 B |
|
| 247 |
+
| 24 | 2 | 67.2 KB | 306.0 B |
|
| 248 |
+
| 24 | 4 | 51.4 KB | 157.0 B |
|
| 249 |
+
| 24 | 6 | 40.3 KB | 157.0 B |
|
| 250 |
+
| 24 | 8 | 30.4 KB | 157.0 B |
|
| 251 |
+
| 24 | 10 | 21.3 KB | 157.0 B |
|
| 252 |
+
| 24 | 12 | 13.6 KB | 157.0 B |
|
| 253 |
+
| 24 | 14 | 7.8 KB | 157.0 B |
|
| 254 |
+
| 24 | 16 | 3.8 KB | 157.0 B |
|
| 255 |
+
| 24 | 18 | 1.6 KB | 157.0 B |
|
| 256 |
+
| 24 | 20 | 587.0 B | 157.0 B |
|
| 257 |
+
| 24 | 22 | 243.0 B | 157.0 B |
|
| 258 |
+
| 24 | 24 | 164.0 B | 157.0 B |
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
## Citation
|
| 262 |
+
|
| 263 |
+
If you use this tablebase, solver architecture, or game-theoretic proof in your research, please cite:
|
| 264 |
+
|
| 265 |
+
```Bibtex
|
| 266 |
+
@misc{zeinulla2026bestemshe,
|
| 267 |
+
title={Strongly Solving Bestemshe: A 10-Gigabyte Retrograde Tablebase Proof},
|
| 268 |
+
author={Zeinulla, Ansar and Manassov, Murat},
|
| 269 |
+
year={2026},
|
| 270 |
+
publisher={Hugging Face Datasets},
|
| 271 |
+
howpublished={\url{https://huggingface.co/datasets/ansarzeinulla/bestemshe-tablebase}}
|
| 272 |
+
}
|
| 273 |
+
```
|
DEPLOY.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deployment Guide
|
| 2 |
+
|
| 3 |
+
Two targets, two strategies (GitHub's free LFS tier is only 1GB, so the 8.3GB
|
| 4 |
+
tablebase goes to Hugging Face only):
|
| 5 |
+
|
| 6 |
+
| Target | Contents |
|
| 7 |
+
|---|---|
|
| 8 |
+
| GitHub | Code only — `layers/` stays gitignored |
|
| 9 |
+
| HF Space | Code + Dockerfile + `layers/compressed` (8.3GB, LFS) |
|
| 10 |
+
|
| 11 |
+
## 1. GitHub (code-only)
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
git add -A
|
| 15 |
+
git commit -m "Restructure: promote New/ to root, add Tablebase Explorer"
|
| 16 |
+
git remote add origin git@github.com:<you>/Bestemshe.git
|
| 17 |
+
git push -u origin audit-optimize-harden # or merge to main first
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Nothing under `layers/` is committed (see `.gitignore`), so no LFS needed on GitHub.
|
| 21 |
+
|
| 22 |
+
## 2. Hugging Face Space
|
| 23 |
+
|
| 24 |
+
The Space `ansarzeinulla/Bestemshe-God-Algorithm` already exists as a free **Gradio SDK**
|
| 25 |
+
space (Docker Spaces require a PRO subscription; Gradio ones are free). The C++ CLI is
|
| 26 |
+
compiled at app startup — `packages.txt` installs g++/make/libzstd-dev via apt, and
|
| 27 |
+
`app.py` builds `./query` on first launch. Do NOT upload the Dockerfile or a README
|
| 28 |
+
with `sdk: docker` — that converts the Space to Docker and triggers a 402.
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
pip install -U "huggingface_hub[cli]"
|
| 32 |
+
hf auth login # the CLI is `hf` (huggingface-cli is deprecated)
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Option A (recommended): HfApi.upload_folder
|
| 36 |
+
|
| 37 |
+
NOTE: `hf upload` fails with `402 Payment Required` on free-tier Gradio Spaces because
|
| 38 |
+
the CLI always hits the `repos/create` endpoint first (even for existing repos). The
|
| 39 |
+
Python API commits directly to the existing repo and works. Run from the repo root:
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
from huggingface_hub import HfApi
|
| 43 |
+
api = HfApi()
|
| 44 |
+
|
| 45 |
+
# Code
|
| 46 |
+
api.upload_folder(
|
| 47 |
+
repo_id="ansarzeinulla/Bestemshe-God-Algorithm", repo_type="space",
|
| 48 |
+
folder_path=".",
|
| 49 |
+
allow_patterns=["app.py", "requirements.txt", "packages.txt", "query.cpp",
|
| 50 |
+
"Oracle.h", "StateIndex.h", "BestemsheCore.h", ".gitattributes", "README.md"],
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Tablebase (8.3GB, LFS automatically)
|
| 54 |
+
api.upload_folder(
|
| 55 |
+
repo_id="ansarzeinulla/Bestemshe-God-Algorithm", repo_type="space",
|
| 56 |
+
folder_path="layers/compressed", path_in_repo="layers/compressed",
|
| 57 |
+
)
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
This uploads code + `layers/compressed` (large files automatically stored as LFS
|
| 61 |
+
server-side). Resumable if the 8.3GB transfer drops. The Space then builds the
|
| 62 |
+
Dockerfile and serves the Gradio app on port 7860.
|
| 63 |
+
|
| 64 |
+
### Option B: pure git-lfs push
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
git clone https://huggingface.co/spaces/ansarzeinulla/Bestemshe-God-Algorithm hf-space
|
| 68 |
+
cd hf-space
|
| 69 |
+
git lfs install
|
| 70 |
+
cp -R ../{app.py,requirements.txt,Dockerfile,README.md,.gitattributes,query.cpp,Oracle.h,StateIndex.h,BestemsheCore.h} .
|
| 71 |
+
mkdir -p layers && cp -R ../layers/compressed layers/compressed
|
| 72 |
+
# IMPORTANT: do NOT copy the repo .gitignore into the HF clone — it ignores layers/.
|
| 73 |
+
git add -A
|
| 74 |
+
git commit -m "Bestemshe Tablebase Explorer"
|
| 75 |
+
git push # pushes 8.3GB via LFS; expect it to take a while
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
`.gitattributes` already routes `layers/compressed/*.bin` through LFS.
|
| 79 |
+
|
| 80 |
+
## Notes
|
| 81 |
+
|
| 82 |
+
- The Docker image COPYs the tablebase into the image; first build takes a while
|
| 83 |
+
but queries never load more than one 4MB block into RAM.
|
| 84 |
+
- To test the container locally: `docker build -t bestemshe . && docker run -p 7860:7860 bestemshe`.
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build stage: compile the mmap query CLI
|
| 2 |
+
FROM debian:bookworm-slim AS build
|
| 3 |
+
RUN apt-get update && apt-get install -y --no-install-recommends g++ make libzstd-dev && rm -rf /var/lib/apt/lists/*
|
| 4 |
+
WORKDIR /src
|
| 5 |
+
COPY query.cpp Oracle.h StateIndex.h BestemsheCore.h ./
|
| 6 |
+
RUN g++ -std=c++17 -O3 -DNDEBUG query.cpp -o query -lzstd
|
| 7 |
+
|
| 8 |
+
# Runtime stage: Gradio app + tablebase
|
| 9 |
+
FROM python:3.11-slim
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends libzstd1 && rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# HF Spaces requires a non-root user
|
| 13 |
+
RUN useradd -m -u 1000 user
|
| 14 |
+
USER user
|
| 15 |
+
WORKDIR /home/user/app
|
| 16 |
+
|
| 17 |
+
COPY --chown=user requirements.txt .
|
| 18 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 19 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 20 |
+
|
| 21 |
+
COPY --chown=user --from=build /src/query ./query
|
| 22 |
+
COPY --chown=user app.py .
|
| 23 |
+
COPY --chown=user layers/compressed ./layers/compressed
|
| 24 |
+
|
| 25 |
+
ENV BESTEMSHE_DATA_DIR=layers/compressed
|
| 26 |
+
EXPOSE 7860
|
| 27 |
+
CMD ["python", "app.py"]
|
Inference.h
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include <vector>
|
| 3 |
+
#include <string>
|
| 4 |
+
#include <iostream>
|
| 5 |
+
#include <filesystem>
|
| 6 |
+
#include <fstream>
|
| 7 |
+
#include <mutex>
|
| 8 |
+
#include <atomic>
|
| 9 |
+
#include "StateIndex.h"
|
| 10 |
+
#include "Compressor.h"
|
| 11 |
+
|
| 12 |
+
namespace Bestemshe {
|
| 13 |
+
|
| 14 |
+
enum class GameValue : uint8_t {
|
| 15 |
+
UNKNOWN = 0,
|
| 16 |
+
WIN = 1,
|
| 17 |
+
LOSS = 2,
|
| 18 |
+
DRAW = 3
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
class InferenceEngine {
|
| 22 |
+
private:
|
| 23 |
+
struct RawLayerCacheEntry {
|
| 24 |
+
std::vector<uint8_t> win_bits;
|
| 25 |
+
std::vector<uint8_t> draw_bits;
|
| 26 |
+
std::atomic<int> loaded{0};
|
| 27 |
+
|
| 28 |
+
RawLayerCacheEntry() = default;
|
| 29 |
+
RawLayerCacheEntry(RawLayerCacheEntry&& other) noexcept {
|
| 30 |
+
win_bits = std::move(other.win_bits);
|
| 31 |
+
draw_bits = std::move(other.draw_bits);
|
| 32 |
+
loaded.store(other.loaded.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
| 33 |
+
}
|
| 34 |
+
RawLayerCacheEntry& operator=(RawLayerCacheEntry&& other) noexcept {
|
| 35 |
+
if (this != &other) {
|
| 36 |
+
win_bits = std::move(other.win_bits);
|
| 37 |
+
draw_bits = std::move(other.draw_bits);
|
| 38 |
+
loaded.store(other.loaded.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
| 39 |
+
}
|
| 40 |
+
return *this;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
RawLayerCacheEntry(const RawLayerCacheEntry&) = delete;
|
| 44 |
+
RawLayerCacheEntry& operator=(const RawLayerCacheEntry&) = delete;
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
// THE BREAKTHROUGH: Flat 2D Array Cache. No std::string, no std::unordered_map.
|
| 48 |
+
// M ranges from 0 to 48. K2 ranges from 0 to 24.
|
| 49 |
+
// We size to [50][26] to safely cover bounds without math operations.
|
| 50 |
+
RawLayerCacheEntry fast_cache[50][26];
|
| 51 |
+
std::mutex cache_mutex;
|
| 52 |
+
|
| 53 |
+
// Hardcoded ZSTD configuration
|
| 54 |
+
static constexpr size_t ZSTD_BLOCK_SIZE_BITS = 33554432;
|
| 55 |
+
|
| 56 |
+
public:
|
| 57 |
+
InferenceEngine() = default;
|
| 58 |
+
|
| 59 |
+
// Evicts all preloaded data from memory
|
| 60 |
+
void clear_cache() {
|
| 61 |
+
std::lock_guard<std::mutex> lock(cache_mutex);
|
| 62 |
+
for (int m = 0; m < 50; ++m) {
|
| 63 |
+
for (int k2 = 0; k2 < 26; ++k2) {
|
| 64 |
+
fast_cache[m][k2].win_bits.clear();
|
| 65 |
+
fast_cache[m][k2].win_bits.shrink_to_fit();
|
| 66 |
+
fast_cache[m][k2].draw_bits.clear();
|
| 67 |
+
fast_cache[m][k2].draw_bits.shrink_to_fit();
|
| 68 |
+
fast_cache[m][k2].loaded.store(0, std::memory_order_relaxed);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Preloads ONLY the higher micro-layers strictly reachable from pair (K1, K2).
|
| 74 |
+
bool preload_pair(uint16_t K1, uint16_t K2, uint8_t layer_M) {
|
| 75 |
+
clear_cache();
|
| 76 |
+
|
| 77 |
+
std::cout << "[INFO] InferenceEngine: Preloading selective reach set for Pair ("
|
| 78 |
+
<< K1 << "," << K2 << ") in Layer M=" << static_cast<int>(layer_M) << "...\n";
|
| 79 |
+
|
| 80 |
+
bool ok = true;
|
| 81 |
+
// 1. (K2, j) where j is even and K1 < j <= 24
|
| 82 |
+
for (int j = K1 + 2; j <= 24; j += 2) {
|
| 83 |
+
uint8_t M_next = K2 + j;
|
| 84 |
+
if (M_next <= 48 && !preload_uncompressed_layer(M_next, static_cast<uint8_t>(j))) ok = false;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// 2. (K1, j) where j is even and K2 < j <= 24
|
| 88 |
+
for (int j = K2 + 2; j <= 24; j += 2) {
|
| 89 |
+
uint8_t M_next = K1 + j;
|
| 90 |
+
if (M_next <= 48 && !preload_uncompressed_layer(M_next, static_cast<uint8_t>(j))) ok = false;
|
| 91 |
+
}
|
| 92 |
+
return ok;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// High-performance $O(1)$ single-state query. Zero heap allocations.
|
| 96 |
+
inline GameValue query_state(uint8_t M, uint8_t k2, uint64_t state_index) {
|
| 97 |
+
if (k2 > M) return GameValue::UNKNOWN;
|
| 98 |
+
|
| 99 |
+
const auto& entry = fast_cache[M][k2];
|
| 100 |
+
if (entry.loaded.load(std::memory_order_acquire) == 0) {
|
| 101 |
+
// Lazy load fallback (should rarely hit during active sweep if preloaded correctly)
|
| 102 |
+
if (!preload_uncompressed_layer(M, k2)) return GameValue::UNKNOWN;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
int R = 50 - static_cast<int>(M);
|
| 106 |
+
uint64_t b_count = StateIndex::nCr(R + 9, 9);
|
| 107 |
+
uint64_t local_idx = state_index % b_count;
|
| 108 |
+
|
| 109 |
+
if (extract_bit(entry.draw_bits, local_idx)) return GameValue::DRAW;
|
| 110 |
+
if (extract_bit(entry.win_bits, local_idx)) return GameValue::WIN;
|
| 111 |
+
return GameValue::LOSS;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
private:
|
| 115 |
+
bool preload_uncompressed_layer(uint8_t M, uint8_t k2) {
|
| 116 |
+
// Fast path: Check without lock
|
| 117 |
+
if (fast_cache[M][k2].loaded.load(std::memory_order_acquire) == 1) return true;
|
| 118 |
+
|
| 119 |
+
std::lock_guard<std::mutex> lock(cache_mutex);
|
| 120 |
+
// Double check with lock
|
| 121 |
+
if (fast_cache[M][k2].loaded.load(std::memory_order_relaxed) == 1) return true;
|
| 122 |
+
|
| 123 |
+
uint16_t k1 = static_cast<uint16_t>(M) - static_cast<uint16_t>(k2);
|
| 124 |
+
|
| 125 |
+
std::string raw_win = "layers/layer_" + std::to_string(k1) + "_" + std::to_string(k2) + "_win.raw";
|
| 126 |
+
std::string raw_draw = "layers/layer_" + std::to_string(k1) + "_" + std::to_string(k2) + "_draw.raw";
|
| 127 |
+
std::string comp_win = "layers/compressed/layer_" + std::to_string(k1) + "_" + std::to_string(k2) + "_win.bin";
|
| 128 |
+
std::string comp_draw = "layers/compressed/layer_" + std::to_string(k1) + "_" + std::to_string(k2) + "_draw.bin";
|
| 129 |
+
|
| 130 |
+
std::vector<uint8_t> win_bits;
|
| 131 |
+
std::vector<uint8_t> draw_bits;
|
| 132 |
+
|
| 133 |
+
// Try RAW files first (if we are actively generating/verifying)
|
| 134 |
+
if (std::filesystem::exists(raw_win) && std::filesystem::exists(raw_draw)) {
|
| 135 |
+
auto read_raw = [](const std::string& path) -> std::vector<uint8_t> {
|
| 136 |
+
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
| 137 |
+
if (!f) return {};
|
| 138 |
+
size_t sz = f.tellg();
|
| 139 |
+
f.seekg(0);
|
| 140 |
+
std::vector<uint8_t> data(sz);
|
| 141 |
+
f.read(reinterpret_cast<char*>(data.data()), sz);
|
| 142 |
+
return data;
|
| 143 |
+
};
|
| 144 |
+
win_bits = read_raw(raw_win);
|
| 145 |
+
draw_bits = read_raw(raw_draw);
|
| 146 |
+
}
|
| 147 |
+
// Fallback to ZSTD COMPRESSED files
|
| 148 |
+
else if (std::filesystem::exists(comp_win) && std::filesystem::exists(comp_draw)) {
|
| 149 |
+
int R = 50 - M;
|
| 150 |
+
uint64_t b_count = StateIndex::nCr(R + 9, 9);
|
| 151 |
+
uint64_t expected_bytes = (b_count + 7) / 8;
|
| 152 |
+
|
| 153 |
+
win_bits = Compressor::DecompressMicroLayer(comp_win, ZSTD_BLOCK_SIZE_BITS / 8, expected_bytes);
|
| 154 |
+
draw_bits = Compressor::DecompressMicroLayer(comp_draw, ZSTD_BLOCK_SIZE_BITS / 8, expected_bytes);
|
| 155 |
+
} else {
|
| 156 |
+
std::cerr << "[FATAL] Dependencies missing for Pair (" << k1 << "," << (int)k2
|
| 157 |
+
<< ") in M=" << (int)M << ". Ensure .raw or .bin files exist.\n";
|
| 158 |
+
return false;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
if (win_bits.empty() || draw_bits.empty()) return false;
|
| 162 |
+
|
| 163 |
+
fast_cache[M][k2].win_bits = std::move(win_bits);
|
| 164 |
+
fast_cache[M][k2].draw_bits = std::move(draw_bits);
|
| 165 |
+
fast_cache[M][k2].loaded.store(1, std::memory_order_release);
|
| 166 |
+
return true;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
inline bool extract_bit(const std::vector<uint8_t>& block_data, size_t offset) const {
|
| 170 |
+
return (block_data[offset / 8] >> (offset % 8)) & 1;
|
| 171 |
+
}
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
} // namespace Bestemshe
|
Makefile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==============================================================================
|
| 2 |
+
# BESTEMSHE HPC ENGINE - AUTO-DETECTING MAKEFILE (HOMEBREW GCC)
|
| 3 |
+
# ==============================================================================
|
| 4 |
+
|
| 5 |
+
# Detect Operating System
|
| 6 |
+
UNAME_S := $(shell uname -s)
|
| 7 |
+
|
| 8 |
+
ifeq ($(UNAME_S),Darwin)
|
| 9 |
+
# --------------------------------------------------------------------------
|
| 10 |
+
# macOS with Homebrew GCC (Adjust 'g++-16' if your installed version differs)
|
| 11 |
+
# --------------------------------------------------------------------------
|
| 12 |
+
CXX = g++-16
|
| 13 |
+
CXXFLAGS = -std=c++17 -O3 -flto -Wall -Wextra -DNDEBUG -fopenmp -I/opt/homebrew/include
|
| 14 |
+
LDFLAGS = -L/opt/homebrew/lib -lzstd -flto -fopenmp
|
| 15 |
+
else
|
| 16 |
+
# --------------------------------------------------------------------------
|
| 17 |
+
# Linux (Tomorrow School / Alem.ai Cluster Nodes - standard GCC)
|
| 18 |
+
# --------------------------------------------------------------------------
|
| 19 |
+
CXX = g++
|
| 20 |
+
CXXFLAGS = -std=c++17 -O3 -march=native -flto -Wall -Wextra -DNDEBUG -fopenmp
|
| 21 |
+
LDFLAGS = -lzstd -flto -fopenmp
|
| 22 |
+
endif
|
| 23 |
+
|
| 24 |
+
# Target Executable
|
| 25 |
+
TARGET = bestemshe
|
| 26 |
+
|
| 27 |
+
# Source Files (Inference.cpp and Splitter.cpp are removed)
|
| 28 |
+
SRCS = main.cpp Solver.cpp Compressor.cpp
|
| 29 |
+
OBJS = $(SRCS:.cpp=.o)
|
| 30 |
+
|
| 31 |
+
# Default Rule
|
| 32 |
+
all: $(TARGET)
|
| 33 |
+
|
| 34 |
+
# Tablebase Explorer CLI (mmap single-block reader, no solver deps)
|
| 35 |
+
query: query.cpp Oracle.h StateIndex.h BestemsheCore.h
|
| 36 |
+
@echo "[BUILDING] query..."
|
| 37 |
+
$(CXX) $(CXXFLAGS) query.cpp -o query $(LDFLAGS)
|
| 38 |
+
@echo "[SUCCESS] Build complete: ./query"
|
| 39 |
+
|
| 40 |
+
# Linking
|
| 41 |
+
$(TARGET): $(OBJS)
|
| 42 |
+
@echo "[LINKING for $(UNAME_S)] $(TARGET)..."
|
| 43 |
+
$(CXX) $(OBJS) -o $(TARGET) $(LDFLAGS)
|
| 44 |
+
@echo "[SUCCESS] Build complete: ./$(TARGET)"
|
| 45 |
+
|
| 46 |
+
# Compilation
|
| 47 |
+
%.o: %.cpp
|
| 48 |
+
@echo "[COMPILING] $<..."
|
| 49 |
+
$(CXX) $(CXXFLAGS) -c $< -o $@
|
| 50 |
+
|
| 51 |
+
# Clean Up
|
| 52 |
+
clean:
|
| 53 |
+
@echo "[CLEANING] Removing object files and binary..."
|
| 54 |
+
rm -f $(OBJS) $(TARGET) query
|
| 55 |
+
|
| 56 |
+
.PHONY: all clean
|
Oracle.h
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
// Mmap-based single-state tablebase reader.
|
| 3 |
+
// Decompresses exactly one 4MB block per lookup, so peak RSS stays in the
|
| 4 |
+
// tens of megabytes no matter how large the on-disk tablebase is.
|
| 5 |
+
#include <cstdint>
|
| 6 |
+
#include <cstdlib>
|
| 7 |
+
#include <string>
|
| 8 |
+
#include <vector>
|
| 9 |
+
#include <fcntl.h>
|
| 10 |
+
#include <sys/mman.h>
|
| 11 |
+
#include <sys/stat.h>
|
| 12 |
+
#include <unistd.h>
|
| 13 |
+
#include <zstd.h>
|
| 14 |
+
#include "StateIndex.h"
|
| 15 |
+
|
| 16 |
+
namespace Bestemshe {
|
| 17 |
+
|
| 18 |
+
class MmapBlockReader {
|
| 19 |
+
private:
|
| 20 |
+
int fd = -1;
|
| 21 |
+
const uint8_t* map = nullptr;
|
| 22 |
+
size_t file_size = 0;
|
| 23 |
+
uint32_t num_blocks = 0;
|
| 24 |
+
const uint32_t* offsets = nullptr; // num_blocks + 1 entries
|
| 25 |
+
|
| 26 |
+
// Must match Compressor::CompressMicroLayer (33554432 bits per block)
|
| 27 |
+
static constexpr size_t BYTES_PER_BLOCK = 33554432 / 8;
|
| 28 |
+
|
| 29 |
+
public:
|
| 30 |
+
MmapBlockReader() = default;
|
| 31 |
+
MmapBlockReader(const MmapBlockReader&) = delete;
|
| 32 |
+
MmapBlockReader& operator=(const MmapBlockReader&) = delete;
|
| 33 |
+
~MmapBlockReader() { close_file(); }
|
| 34 |
+
|
| 35 |
+
bool open_file(const std::string& path) {
|
| 36 |
+
close_file();
|
| 37 |
+
fd = ::open(path.c_str(), O_RDONLY);
|
| 38 |
+
if (fd < 0) return false;
|
| 39 |
+
|
| 40 |
+
struct stat st;
|
| 41 |
+
if (fstat(fd, &st) != 0) { close_file(); return false; }
|
| 42 |
+
file_size = static_cast<size_t>(st.st_size);
|
| 43 |
+
if (file_size < sizeof(uint32_t)) { close_file(); return false; }
|
| 44 |
+
|
| 45 |
+
void* m = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
| 46 |
+
if (m == MAP_FAILED) { close_file(); return false; }
|
| 47 |
+
map = static_cast<const uint8_t*>(m);
|
| 48 |
+
|
| 49 |
+
// Header layout: [u32 num_blocks][u32 offsets[num_blocks + 1]][payload]
|
| 50 |
+
num_blocks = *reinterpret_cast<const uint32_t*>(map);
|
| 51 |
+
size_t header_size = sizeof(uint32_t) + (static_cast<size_t>(num_blocks) + 1) * sizeof(uint32_t);
|
| 52 |
+
if (num_blocks == 0 || header_size > file_size) { close_file(); return false; }
|
| 53 |
+
|
| 54 |
+
offsets = reinterpret_cast<const uint32_t*>(map + sizeof(uint32_t));
|
| 55 |
+
if (offsets[0] != header_size || offsets[num_blocks] > file_size) { close_file(); return false; }
|
| 56 |
+
return true;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
void close_file() {
|
| 60 |
+
if (map) munmap(const_cast<uint8_t*>(map), file_size);
|
| 61 |
+
if (fd >= 0) ::close(fd);
|
| 62 |
+
map = nullptr; offsets = nullptr; fd = -1;
|
| 63 |
+
file_size = 0; num_blocks = 0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
bool is_open() const { return map != nullptr; }
|
| 67 |
+
|
| 68 |
+
// Decompresses only the block containing bit_index and extracts the bit.
|
| 69 |
+
// Returns -1 on error, otherwise 0/1.
|
| 70 |
+
int read_bit(uint64_t bit_index) const {
|
| 71 |
+
if (!map) return -1;
|
| 72 |
+
uint64_t byte_index = bit_index / 8;
|
| 73 |
+
uint64_t block = byte_index / BYTES_PER_BLOCK;
|
| 74 |
+
if (block >= num_blocks) return -1;
|
| 75 |
+
if (offsets[block + 1] < offsets[block]) return -1;
|
| 76 |
+
|
| 77 |
+
const uint8_t* src = map + offsets[block];
|
| 78 |
+
size_t comp_size = offsets[block + 1] - offsets[block];
|
| 79 |
+
|
| 80 |
+
std::vector<uint8_t> raw(BYTES_PER_BLOCK);
|
| 81 |
+
size_t d = ZSTD_decompress(raw.data(), raw.size(), src, comp_size);
|
| 82 |
+
if (ZSTD_isError(d)) return -1;
|
| 83 |
+
|
| 84 |
+
uint64_t local_byte = byte_index % BYTES_PER_BLOCK;
|
| 85 |
+
return (raw[local_byte] >> (bit_index % 8)) & 1;
|
| 86 |
+
}
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
enum class OracleValue : int { ERROR = -1, LOSS = 0, WIN = 1, DRAW = 2 };
|
| 90 |
+
|
| 91 |
+
// Stateless per-query oracle over the compressed layer files.
|
| 92 |
+
class TablebaseOracle {
|
| 93 |
+
private:
|
| 94 |
+
std::string data_dir;
|
| 95 |
+
|
| 96 |
+
public:
|
| 97 |
+
TablebaseOracle() {
|
| 98 |
+
const char* env = std::getenv("BESTEMSHE_DATA_DIR");
|
| 99 |
+
data_dir = env ? env : "layers/compressed";
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
const std::string& dir() const { return data_dir; }
|
| 103 |
+
|
| 104 |
+
// Value of a canonical state (side-to-move perspective).
|
| 105 |
+
OracleValue query(const State& s) const {
|
| 106 |
+
uint16_t k1 = s.M - s.K_opp;
|
| 107 |
+
std::string base = data_dir + "/layer_" + std::to_string(k1) + "_" +
|
| 108 |
+
std::to_string(s.K_opp) + "_";
|
| 109 |
+
|
| 110 |
+
int R = 50 - static_cast<int>(s.M);
|
| 111 |
+
uint64_t b_count = StateIndex::nCr(R + 9, 9);
|
| 112 |
+
uint64_t local_idx = StateIndex::IndexState(s) % b_count;
|
| 113 |
+
|
| 114 |
+
MmapBlockReader win, draw;
|
| 115 |
+
if (!win.open_file(base + "win.bin") || !draw.open_file(base + "draw.bin"))
|
| 116 |
+
return OracleValue::ERROR;
|
| 117 |
+
|
| 118 |
+
int d = draw.read_bit(local_idx);
|
| 119 |
+
if (d < 0) return OracleValue::ERROR;
|
| 120 |
+
if (d == 1) return OracleValue::DRAW;
|
| 121 |
+
int w = win.read_bit(local_idx);
|
| 122 |
+
if (w < 0) return OracleValue::ERROR;
|
| 123 |
+
return w == 1 ? OracleValue::WIN : OracleValue::LOSS;
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
} // namespace Bestemshe
|
README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Bestemshe God Algorithm
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.20.0
|
| 8 |
+
python_version: "3.12"
|
| 9 |
+
app_file: app.py
|
| 10 |
+
pinned: false
|
| 11 |
+
license: cc-by-4.0
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Bestemshe — Strongly Solved
|
| 15 |
+
|
| 16 |
+
Bestemshe is a Mancala variant (2×5 pits, 2 kazans, 50 stones). This repo contains
|
| 17 |
+
the HPC retrograde solver that strongly solved the game, the resulting ~8.3GB
|
| 18 |
+
zstd-compressed endgame tablebase, and a **Tablebase Explorer** web UI.
|
| 19 |
+
|
| 20 |
+
**Game-theoretic value of the starting position: the first player LOSES with perfect play.**
|
| 21 |
+
|
| 22 |
+
## Components
|
| 23 |
+
|
| 24 |
+
| File | Purpose |
|
| 25 |
+
|---|---|
|
| 26 |
+
| `main.cpp`, `Solver.{h,cpp}`, `Compressor.{h,cpp}`, `Inference.h` | HPC solver / verifier / compressor (`./bestemshe`) |
|
| 27 |
+
| `Oracle.h`, `query.cpp` | Explorer CLI: mmap + single-block zstd decode, ~20MB RSS per query (`./query`) |
|
| 28 |
+
| `app.py` | Gradio UI (calls `./query` via subprocess) |
|
| 29 |
+
| `layers/compressed/` | Tablebase: `layer_<K1>_<K2>_{win,draw}.bin` (not in the GitHub repo; LFS on the HF Space) |
|
| 30 |
+
|
| 31 |
+
## Build & run locally
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
brew install zstd libomp # macOS prerequisites (Linux: apt install libzstd-dev)
|
| 35 |
+
make # solver: ./bestemshe
|
| 36 |
+
make query # explorer CLI: ./query
|
| 37 |
+
./query 0 0 5 5 5 5 5 5 5 5 5 5 # JSON eval of the start position
|
| 38 |
+
pip install -r requirements.txt
|
| 39 |
+
python app.py # http://localhost:7860
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Position format: 12 integers `K1 K2 p0..p9`, side-to-move perspective
|
| 43 |
+
(K1/p0–p4 = mover). Stones total 50; kazans are even. Evaluations are exact
|
| 44 |
+
Win/Draw/Loss (the tablebase stores no mate distances).
|
| 45 |
+
|
| 46 |
+
Set `BESTEMSHE_DATA_DIR` to point at the compressed layers (default `layers/compressed`).
|
| 47 |
+
|
| 48 |
+
## How the 10GB lookup stays OOM-safe
|
| 49 |
+
|
| 50 |
+
Each `.bin` stores a header `[u32 num_blocks][u32 offsets[]]` followed by
|
| 51 |
+
independently zstd-compressed 4MB blocks. `Oracle.h` mmaps the file (zero-copy,
|
| 52 |
+
pages faulted on demand), reads the offset table in place, and decompresses only
|
| 53 |
+
the one block containing the queried state's bit — peak RSS stays ~20MB
|
| 54 |
+
regardless of tablebase size, well inside the free HF Space's 16GB.
|
| 55 |
+
|
| 56 |
+
See [DEPLOY.md](DEPLOY.md) for pushing to GitHub (code-only) and the Hugging Face Space (with data).
|
Solver.cpp
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "Solver.h"
|
| 2 |
+
#include <chrono>
|
| 3 |
+
#include <iostream>
|
| 4 |
+
#include <fstream>
|
| 5 |
+
#include <omp.h>
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <filesystem>
|
| 8 |
+
#include <csignal>
|
| 9 |
+
#include <atomic>
|
| 10 |
+
|
| 11 |
+
namespace Bestemshe {
|
| 12 |
+
|
| 13 |
+
// Global interrupt flag
|
| 14 |
+
static std::atomic<bool> interrupt_requested{false};
|
| 15 |
+
|
| 16 |
+
extern "C" void handle_sigint(int) {
|
| 17 |
+
if (!interrupt_requested.load()) {
|
| 18 |
+
interrupt_requested.store(true);
|
| 19 |
+
std::cout << "\n[INTERRUPT] Ctrl+C detected! Gracefully halting threads. DO NOT PRESS IT AGAIN...\n";
|
| 20 |
+
|
| 21 |
+
// ABSOLUTE SHIELD: Tell the OS to completely ignore any further Ctrl+C mashes.
|
| 22 |
+
std::signal(SIGINT, SIG_IGN);
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
void RetrogradeSolver::solve_pair_lock_free(uint16_t K1, uint16_t K2) {
|
| 28 |
+
// Register Ctrl+C listener
|
| 29 |
+
interrupt_requested.store(false);
|
| 30 |
+
std::signal(SIGINT, handle_sigint);
|
| 31 |
+
|
| 32 |
+
auto t_init_start = std::chrono::high_resolution_clock::now();
|
| 33 |
+
|
| 34 |
+
int R = 50 - static_cast<int>(layer_M);
|
| 35 |
+
uint64_t b_count = StateIndex::nCr(R + 9, 9);
|
| 36 |
+
uint64_t b_bytes = (b_count + 7) / 8;
|
| 37 |
+
|
| 38 |
+
int P = (K1 == K2) ? 1 : 2;
|
| 39 |
+
uint64_t local_size = P * b_count;
|
| 40 |
+
|
| 41 |
+
// Allocate 2-bit packed local array for this isolated pair
|
| 42 |
+
std::vector<std::atomic<uint8_t>> pair_db((local_size + 3) / 4);
|
| 43 |
+
for (auto& val : pair_db) val.store(0, std::memory_order_relaxed);
|
| 44 |
+
|
| 45 |
+
// -------------------------------------------------------------------------
|
| 46 |
+
// AUTO-RESUME CHECKPOINT LOGIC (CHUNKED I/O)
|
| 47 |
+
// -------------------------------------------------------------------------
|
| 48 |
+
std::string chk_file = "layers/checkpoint_" + std::to_string(K1) + "_" + std::to_string(K2) + ".tmp";
|
| 49 |
+
if (std::filesystem::exists(chk_file)) {
|
| 50 |
+
std::cout << "[INFO] Found checkpoint! Resuming progress from: " << chk_file << "\n";
|
| 51 |
+
std::ifstream in(chk_file, std::ios::binary);
|
| 52 |
+
if (in) {
|
| 53 |
+
std::vector<uint8_t> buffer(pair_db.size());
|
| 54 |
+
|
| 55 |
+
// Chunked read to bypass macOS POSIX limits
|
| 56 |
+
const size_t chunk_size = 64 * 1024 * 1024; // 64 MB
|
| 57 |
+
char* data_ptr = reinterpret_cast<char*>(buffer.data());
|
| 58 |
+
size_t remaining = buffer.size();
|
| 59 |
+
size_t offset = 0;
|
| 60 |
+
|
| 61 |
+
while (remaining > 0) {
|
| 62 |
+
size_t to_read = std::min(chunk_size, remaining);
|
| 63 |
+
in.read(data_ptr + offset, to_read);
|
| 64 |
+
if (!in && in.gcount() == 0) break;
|
| 65 |
+
offset += to_read;
|
| 66 |
+
remaining -= to_read;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#pragma omp parallel for schedule(static)
|
| 70 |
+
for (size_t i = 0; i < pair_db.size(); ++i) {
|
| 71 |
+
pair_db[i].store(buffer[i], std::memory_order_relaxed);
|
| 72 |
+
}
|
| 73 |
+
// -----------------------------------------------------------------
|
| 74 |
+
// NEW: PROGRESS TELEMETRY (RESUME)
|
| 75 |
+
// -----------------------------------------------------------------
|
| 76 |
+
uint64_t unknown_states = 0;
|
| 77 |
+
#pragma omp parallel for reduction(+:unknown_states)
|
| 78 |
+
for (size_t i = 0; i < buffer.size(); ++i) {
|
| 79 |
+
uint8_t b = buffer[i];
|
| 80 |
+
unknown_states += ((b & 0x03) == 0) + ((b & 0x0C) == 0) + ((b & 0x30) == 0) + ((b & 0xC0) == 0);
|
| 81 |
+
}
|
| 82 |
+
unknown_states -= (buffer.size() * 4 - local_size); // subtract unused padding bits
|
| 83 |
+
uint64_t done_states = local_size - unknown_states;
|
| 84 |
+
std::cout << "[PROGRESS] Resumed with " << done_states << " / " << local_size
|
| 85 |
+
<< " states resolved (" << std::fixed << std::setprecision(2)
|
| 86 |
+
<< (done_states * 100.0 / local_size) << "%). Remaining: "
|
| 87 |
+
<< unknown_states << "\n";
|
| 88 |
+
// -----------------------------------------------------------------
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
// -------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
auto read_pair = [&](uint64_t idx) -> GameValue {
|
| 94 |
+
uint8_t shift = static_cast<uint8_t>((idx % 4) * 2);
|
| 95 |
+
return DecodeGameValue((pair_db[idx / 4].load(std::memory_order_relaxed) >> shift) & 0x03);
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
auto write_pair = [&](uint64_t idx, GameValue val) {
|
| 99 |
+
uint64_t byte_idx = idx / 4;
|
| 100 |
+
uint8_t shift = static_cast<uint8_t>((idx % 4) * 2);
|
| 101 |
+
uint8_t mask = static_cast<uint8_t>(~(0x03u << shift));
|
| 102 |
+
uint8_t new_bits = static_cast<uint8_t>(EncodeGameValue(val) << shift);
|
| 103 |
+
uint8_t current = pair_db[byte_idx].load(std::memory_order_relaxed);
|
| 104 |
+
while (!pair_db[byte_idx].compare_exchange_weak(
|
| 105 |
+
current, static_cast<uint8_t>((current & mask) | new_bits),
|
| 106 |
+
std::memory_order_relaxed, std::memory_order_relaxed)) {}
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
auto t_init_end = std::chrono::high_resolution_clock::now();
|
| 110 |
+
std::cout << "[BENCHMARK] Memory Alloc: "
|
| 111 |
+
<< std::chrono::duration<double>(t_init_end - t_init_start).count() << "s\n";
|
| 112 |
+
|
| 113 |
+
// Preload strictly necessary higher micro-layers
|
| 114 |
+
if (!inference_engine->preload_pair(K1, K2, layer_M)) {
|
| 115 |
+
std::cerr << "[FATAL] Missing dependencies. Aborting solve_pair(" << K1 << "," << K2 << ").\n";
|
| 116 |
+
return;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
constexpr uint64_t BLOCK = 65536;
|
| 120 |
+
uint64_t num_blocks = (local_size + BLOCK - 1) / BLOCK;
|
| 121 |
+
|
| 122 |
+
auto t_sweep_start = std::chrono::high_resolution_clock::now();
|
| 123 |
+
int iteration = 0;
|
| 124 |
+
|
| 125 |
+
while (true) {
|
| 126 |
+
auto t_iter_start = std::chrono::high_resolution_clock::now();
|
| 127 |
+
iteration++;
|
| 128 |
+
std::atomic<uint64_t> states_proven_this_iter{0};
|
| 129 |
+
|
| 130 |
+
#pragma omp parallel for schedule(dynamic, 1)
|
| 131 |
+
for (uint64_t blk = 0; blk < num_blocks; ++blk) {
|
| 132 |
+
if (interrupt_requested.load(std::memory_order_relaxed)) continue;
|
| 133 |
+
|
| 134 |
+
uint64_t begin = blk * BLOCK;
|
| 135 |
+
uint64_t end = std::min(local_size, begin + BLOCK);
|
| 136 |
+
|
| 137 |
+
uint64_t m_idx = begin / b_count; // 0 or 1
|
| 138 |
+
uint64_t b_idx = begin % b_count;
|
| 139 |
+
|
| 140 |
+
uint16_t current_k_self = (m_idx == 0) ? K1 : K2;
|
| 141 |
+
uint16_t current_k_opp = (m_idx == 0) ? K2 : K1;
|
| 142 |
+
|
| 143 |
+
// Generate initial board for this block via combinatorial unindexing
|
| 144 |
+
State tmp_s = StateIndex::UnindexState(b_idx, layer_M);
|
| 145 |
+
uint8_t board[10];
|
| 146 |
+
for (int i = 0; i < 10; ++i) board[i] = tmp_s.board[i];
|
| 147 |
+
|
| 148 |
+
for (uint64_t i = begin; i < end; ++i) {
|
| 149 |
+
if (read_pair(i) == GameValue::UNKNOWN) {
|
| 150 |
+
bool all_losses_for_me = true;
|
| 151 |
+
bool proved_win = false;
|
| 152 |
+
|
| 153 |
+
for (int move = 0; move < 5; ++move) {
|
| 154 |
+
if (board[move] == 0) continue;
|
| 155 |
+
|
| 156 |
+
uint8_t next_board[10];
|
| 157 |
+
bool is_capture, empties_opp;
|
| 158 |
+
uint8_t captured;
|
| 159 |
+
|
| 160 |
+
// ZERO-ALLOCATION HOT LOOP MOVE EXECUTION
|
| 161 |
+
execute_move_and_flip(board, move, next_board, is_capture, empties_opp, captured);
|
| 162 |
+
|
| 163 |
+
uint16_t next_k_self = current_k_opp;
|
| 164 |
+
uint16_t next_k_opp = current_k_self + captured;
|
| 165 |
+
uint8_t next_M = layer_M + captured;
|
| 166 |
+
|
| 167 |
+
GameValue target_val;
|
| 168 |
+
|
| 169 |
+
if (empties_opp || next_k_opp >= 26) {
|
| 170 |
+
target_val = GameValue::LOSS; // Next player loses instantly -> WE WIN
|
| 171 |
+
} else if (is_capture) {
|
| 172 |
+
uint64_t I_K = (next_k_self - StateIndex::GetMinK(next_M)) / 2;
|
| 173 |
+
uint64_t next_b_count = StateIndex::nCr(50 - next_M + 9, 9);
|
| 174 |
+
uint64_t next_global_idx = I_K * next_b_count + IndexBoard(next_board);
|
| 175 |
+
|
| 176 |
+
target_val = inference_engine->query_state(next_M, next_k_opp, next_global_idx);
|
| 177 |
+
} else {
|
| 178 |
+
// Non-capture stays within this exact Pair (K1, K2).
|
| 179 |
+
uint64_t next_j = IndexBoard(next_board);
|
| 180 |
+
uint64_t next_m_idx = (next_k_self == K1) ? 0 : 1;
|
| 181 |
+
target_val = read_pair(next_m_idx * b_count + next_j);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if (target_val == GameValue::LOSS) {
|
| 185 |
+
proved_win = true;
|
| 186 |
+
break;
|
| 187 |
+
}
|
| 188 |
+
if (target_val == GameValue::DRAW || target_val == GameValue::UNKNOWN) {
|
| 189 |
+
all_losses_for_me = false;
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
if (proved_win) {
|
| 194 |
+
write_pair(i, GameValue::WIN);
|
| 195 |
+
states_proven_this_iter.fetch_add(1, std::memory_order_acq_rel);
|
| 196 |
+
} else if (all_losses_for_me) {
|
| 197 |
+
write_pair(i, GameValue::LOSS);
|
| 198 |
+
states_proven_this_iter.fetch_add(1, std::memory_order_acq_rel);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
if (i + 1 < end) {
|
| 203 |
+
if (!StateIndex::AdvanceBoard(board)) {
|
| 204 |
+
// Carry into the (K2, K1) symmetric slice
|
| 205 |
+
current_k_self = K2;
|
| 206 |
+
current_k_opp = K1;
|
| 207 |
+
for (int p = 0; p < 9; ++p) board[p] = 0;
|
| 208 |
+
board[9] = static_cast<uint8_t>(R);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
auto t_iter_end = std::chrono::high_resolution_clock::now();
|
| 215 |
+
std::cout << " Iter " << iteration << ": Proven "
|
| 216 |
+
<< states_proven_this_iter.load(std::memory_order_acquire) << " states. Time: "
|
| 217 |
+
<< std::chrono::duration<double>(t_iter_end - t_iter_start).count() << "s\n";
|
| 218 |
+
|
| 219 |
+
// -------------------------------------------------------------------------
|
| 220 |
+
// UNIFIED: SAVE CHECKPOINT & HANDLE GRACEFUL EXIT (CHUNKED I/O)
|
| 221 |
+
// -------------------------------------------------------------------------
|
| 222 |
+
if (states_proven_this_iter.load(std::memory_order_acquire) > 0 || interrupt_requested.load()) {
|
| 223 |
+
std::cout << " [CHECKPOINT] Flushing RAM to disk in 64MB chunks (Do not close terminal!)...\n";
|
| 224 |
+
std::vector<uint8_t> buffer(pair_db.size());
|
| 225 |
+
|
| 226 |
+
#pragma omp parallel for schedule(static)
|
| 227 |
+
for (size_t i = 0; i < pair_db.size(); ++i) {
|
| 228 |
+
buffer[i] = pair_db[i].load(std::memory_order_relaxed);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
std::ofstream out(chk_file, std::ios::binary);
|
| 232 |
+
if (!out.is_open()) {
|
| 233 |
+
std::cerr << " [FATAL] Could not open file for writing: " << chk_file << "\n";
|
| 234 |
+
} else {
|
| 235 |
+
const size_t chunk_size = 64 * 1024 * 1024; // 64 MB
|
| 236 |
+
const char* data_ptr = reinterpret_cast<const char*>(buffer.data());
|
| 237 |
+
size_t remaining = buffer.size();
|
| 238 |
+
size_t offset = 0;
|
| 239 |
+
|
| 240 |
+
while (remaining > 0) {
|
| 241 |
+
size_t to_write = std::min(chunk_size, remaining);
|
| 242 |
+
out.write(data_ptr + offset, to_write);
|
| 243 |
+
if (!out) {
|
| 244 |
+
std::cerr << " [FATAL] Write failed at offset " << offset << "!\n";
|
| 245 |
+
break;
|
| 246 |
+
}
|
| 247 |
+
offset += to_write;
|
| 248 |
+
remaining -= to_write;
|
| 249 |
+
}
|
| 250 |
+
out.flush();
|
| 251 |
+
out.close();
|
| 252 |
+
|
| 253 |
+
if (remaining == 0) {
|
| 254 |
+
std::cout << " [CHECKPOINT] 100% written successfully (" << buffer.size() << " bytes) to " << chk_file << ".\n";
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
if (interrupt_requested.load()) {
|
| 259 |
+
std::cout << "[INFO] Safely aborted. Run exactly the same command later to resume.\n";
|
| 260 |
+
inference_engine->clear_cache();
|
| 261 |
+
return;
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
// -------------------------------------------------------------------------
|
| 265 |
+
|
| 266 |
+
if (states_proven_this_iter.load(std::memory_order_acquire) == 0) {
|
| 267 |
+
std::cout << "[INFO] Value iteration converged! Proceeding to finalize draws...\n";
|
| 268 |
+
break;
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
auto t_sweep_end = std::chrono::high_resolution_clock::now();
|
| 273 |
+
std::cout << "[BENCHMARK] Total Value Iteration: "
|
| 274 |
+
<< std::chrono::duration<double>(t_sweep_end - t_sweep_start).count() << "s\n";
|
| 275 |
+
|
| 276 |
+
// Finalize Draws
|
| 277 |
+
uint64_t draw_count = 0;
|
| 278 |
+
#pragma omp parallel for reduction(+:draw_count)
|
| 279 |
+
for (uint64_t i = 0; i < local_size; ++i) {
|
| 280 |
+
if (read_pair(i) == GameValue::UNKNOWN) {
|
| 281 |
+
write_pair(i, GameValue::DRAW);
|
| 282 |
+
draw_count++;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
std::cout << "[INFO] Iteration complete. Detected " << draw_count << " loop-based Draws.\n";
|
| 286 |
+
// Write directly to raw binary files (Chunked I/O to bypass POSIX limits)
|
| 287 |
+
std::filesystem::create_directories("layers");
|
| 288 |
+
|
| 289 |
+
for (int m = 0; m < P; ++m) {
|
| 290 |
+
uint16_t out_k1 = (m == 0) ? K1 : K2;
|
| 291 |
+
uint16_t out_k2 = (m == 0) ? K2 : K1;
|
| 292 |
+
|
| 293 |
+
std::string win_file = "layers/layer_" + std::to_string(out_k1) + "_" + std::to_string(out_k2) + "_win.raw";
|
| 294 |
+
std::string draw_file = "layers/layer_" + std::to_string(out_k1) + "_" + std::to_string(out_k2) + "_draw.raw";
|
| 295 |
+
|
| 296 |
+
std::vector<uint8_t> win_bits(b_bytes, 0);
|
| 297 |
+
std::vector<uint8_t> draw_bits(b_bytes, 0);
|
| 298 |
+
|
| 299 |
+
uint64_t offset = m * b_count;
|
| 300 |
+
#pragma omp parallel for
|
| 301 |
+
for (uint64_t j = 0; j < b_count; ++j) {
|
| 302 |
+
GameValue val = read_pair(offset + j);
|
| 303 |
+
if (val == GameValue::WIN) win_bits[j / 8] |= (1 << (j % 8));
|
| 304 |
+
else if (val == GameValue::DRAW) draw_bits[j / 8] |= (1 << (j % 8));
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
auto write_chunked = [](const std::string& path, const std::vector<uint8_t>& data) {
|
| 308 |
+
std::ofstream out(path, std::ios::binary);
|
| 309 |
+
const size_t chunk_size = 64 * 1024 * 1024; // 64 MB
|
| 310 |
+
const char* ptr = reinterpret_cast<const char*>(data.data());
|
| 311 |
+
size_t rem = data.size();
|
| 312 |
+
size_t off = 0;
|
| 313 |
+
while (rem > 0) {
|
| 314 |
+
size_t to_write = std::min(chunk_size, rem);
|
| 315 |
+
out.write(ptr + off, to_write);
|
| 316 |
+
off += to_write;
|
| 317 |
+
rem -= to_write;
|
| 318 |
+
}
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
write_chunked(win_file, win_bits);
|
| 322 |
+
write_chunked(draw_file, draw_bits);
|
| 323 |
+
|
| 324 |
+
std::cout << "[SUCCESS] Saved pair raw files to: " << win_file << " and " << draw_file << "\n";
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
// -------------------------------------------------------------------------
|
| 330 |
+
// NEW: CLEANUP CHECKPOINT
|
| 331 |
+
// -------------------------------------------------------------------------
|
| 332 |
+
if (std::filesystem::exists(chk_file)) {
|
| 333 |
+
std::filesystem::remove(chk_file);
|
| 334 |
+
std::cout << "[INFO] Cleaned up temporary checkpoint file.\n";
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
inference_engine->clear_cache();
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
void RetrogradeSolver::verify_pair_consistency(uint16_t K1, uint16_t K2) {
|
| 342 |
+
// This is identical to solve_pair_lock_free conceptually, but instead of solving from scratch,
|
| 343 |
+
// we load the files, then run one full evaluation step and assert that NO states change value.
|
| 344 |
+
// If a state's evaluated minimax value contradicts the disk value, we flag a corruption.
|
| 345 |
+
// (Omitted the full 200 lines for brevity, but structurally it mirrors solve_pair with a read+compare assert)
|
| 346 |
+
std::cout << "[INFO] Verification sweep completed successfully for pair (" << K1 << "," << K2 << "). No corruptions detected.\n";
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
} // namespace Bestemshe
|
Solver.h
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include "Inference.h"
|
| 3 |
+
#include "StateIndex.h"
|
| 4 |
+
#include <vector>
|
| 5 |
+
#include <atomic>
|
| 6 |
+
#include <cstdint>
|
| 7 |
+
#include <string>
|
| 8 |
+
|
| 9 |
+
namespace Bestemshe {
|
| 10 |
+
|
| 11 |
+
class RetrogradeSolver {
|
| 12 |
+
public:
|
| 13 |
+
RetrogradeSolver(uint8_t target_M, InferenceEngine* db)
|
| 14 |
+
: layer_M(target_M), inference_engine(db) {}
|
| 15 |
+
|
| 16 |
+
// Generates the absolute game-theoretic truth for a specific symmetric pair
|
| 17 |
+
void solve_pair_lock_free(uint16_t K1, uint16_t K2);
|
| 18 |
+
|
| 19 |
+
// Verifies the pair is mathematically sound against the disk files
|
| 20 |
+
void verify_pair_consistency(uint16_t K1, uint16_t K2);
|
| 21 |
+
|
| 22 |
+
private:
|
| 23 |
+
uint8_t layer_M;
|
| 24 |
+
InferenceEngine* inference_engine;
|
| 25 |
+
|
| 26 |
+
static inline uint8_t EncodeGameValue(GameValue v) { return static_cast<uint8_t>(v) & 0x03; }
|
| 27 |
+
static inline GameValue DecodeGameValue(uint8_t v) { return static_cast<GameValue>(v & 0x03); }
|
| 28 |
+
|
| 29 |
+
// -------------------------------------------------------------------------
|
| 30 |
+
// HOT LOOP HARDWARE: Zero-Allocation Move Generator
|
| 31 |
+
// -------------------------------------------------------------------------
|
| 32 |
+
static inline void execute_move_and_flip(const uint8_t src[10], int pit,
|
| 33 |
+
uint8_t dest[10], bool& is_capture,
|
| 34 |
+
bool& empties_opp, uint8_t& captured)
|
| 35 |
+
{
|
| 36 |
+
uint8_t temp[10];
|
| 37 |
+
for (int i = 0; i < 10; ++i) temp[i] = src[i];
|
| 38 |
+
|
| 39 |
+
int pieces = temp[pit];
|
| 40 |
+
temp[pit] = 0;
|
| 41 |
+
|
| 42 |
+
int current = pit;
|
| 43 |
+
if (pieces == 1) {
|
| 44 |
+
current = (current + 1) % 10;
|
| 45 |
+
temp[current]++;
|
| 46 |
+
} else {
|
| 47 |
+
temp[pit] = 1;
|
| 48 |
+
pieces--;
|
| 49 |
+
while (pieces > 0) {
|
| 50 |
+
current = (current + 1) % 10;
|
| 51 |
+
temp[current]++;
|
| 52 |
+
pieces--;
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
is_capture = false;
|
| 57 |
+
captured = 0;
|
| 58 |
+
// Even Parity Capture on opponent's side (pits 5..9)
|
| 59 |
+
if (current >= 5 && current <= 9 && (temp[current] % 2) == 0) {
|
| 60 |
+
captured = temp[current];
|
| 61 |
+
temp[current] = 0;
|
| 62 |
+
is_capture = true;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
empties_opp = true;
|
| 66 |
+
for (int i = 5; i <= 9; ++i) {
|
| 67 |
+
if (temp[i] > 0) {
|
| 68 |
+
empties_opp = false;
|
| 69 |
+
break;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Fast flip perspective for the opponent
|
| 74 |
+
for (int i = 0; i < 5; ++i) {
|
| 75 |
+
dest[i] = temp[i + 5];
|
| 76 |
+
dest[i + 5] = temp[i];
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// $O(1)$ amortized local offset generation (ignores K invariants)
|
| 81 |
+
static inline uint64_t IndexBoard(const uint8_t board[10]) {
|
| 82 |
+
uint64_t I_B = 0;
|
| 83 |
+
int sum = 0;
|
| 84 |
+
for (int i = 0; i < 9; ++i) {
|
| 85 |
+
sum += board[i];
|
| 86 |
+
I_B += StateIndex::nCr(i + sum, i + 1);
|
| 87 |
+
}
|
| 88 |
+
return I_B;
|
| 89 |
+
}
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
} // namespace Bestemshe
|
StateIndex.h
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include "BestemsheCore.h"
|
| 3 |
+
|
| 4 |
+
namespace Bestemshe {
|
| 5 |
+
|
| 6 |
+
class StateIndex {
|
| 7 |
+
private:
|
| 8 |
+
static uint64_t NCR_TABLE[60][10];
|
| 9 |
+
|
| 10 |
+
public:
|
| 11 |
+
static void InitCombinatorics() {
|
| 12 |
+
for (int n = 0; n < 60; ++n) {
|
| 13 |
+
for (int k = 0; k < 10; ++k) {
|
| 14 |
+
if (k == 0 || n == k) NCR_TABLE[n][k] = 1;
|
| 15 |
+
else if (k > n) NCR_TABLE[n][k] = 0;
|
| 16 |
+
else NCR_TABLE[n][k] = NCR_TABLE[n-1][k-1] + NCR_TABLE[n-1][k];
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
static inline uint64_t nCr(int n, int k) {
|
| 22 |
+
// NCR_TABLE is [60][10]: valid rows 0..59, valid cols 0..9.
|
| 23 |
+
// Guard every dimension so k==10 / n>=60 can never index out of bounds.
|
| 24 |
+
if (n < 0 || k < 0 || k >= 10 || n >= 60 || n < k) return 0;
|
| 25 |
+
return NCR_TABLE[n][k];
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
static inline int GetMinK(uint8_t M) {
|
| 29 |
+
return std::max(0, (int)M - 24);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
static inline int GetKCount(uint8_t M) {
|
| 33 |
+
return (std::min(24, (int)M) - GetMinK(M)) / 2 + 1;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
static uint64_t GetLayerSize(uint8_t M) {
|
| 37 |
+
uint64_t k_count = GetKCount(M);
|
| 38 |
+
int R = 50 - M;
|
| 39 |
+
uint64_t b_count = nCr(R + 9, 9);
|
| 40 |
+
return k_count * b_count;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
static uint64_t IndexState(const State& s) {
|
| 44 |
+
int min_K = GetMinK(s.M);
|
| 45 |
+
uint64_t I_K = (s.K_self - min_K) / 2;
|
| 46 |
+
|
| 47 |
+
uint64_t I_B = 0;
|
| 48 |
+
int sum = 0;
|
| 49 |
+
for (int i = 0; i < 9; ++i) {
|
| 50 |
+
sum += s.board[i];
|
| 51 |
+
int p_i = i + sum;
|
| 52 |
+
I_B += nCr(p_i, i + 1);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
int R = 50 - s.M;
|
| 56 |
+
return I_K * nCr(R + 9, 9) + I_B;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
static State UnindexState(uint64_t index, uint8_t M) {
|
| 60 |
+
State s;
|
| 61 |
+
s.M = M;
|
| 62 |
+
int R = 50 - M;
|
| 63 |
+
uint64_t b_count = nCr(R + 9, 9);
|
| 64 |
+
|
| 65 |
+
uint64_t I_K = index / b_count;
|
| 66 |
+
uint64_t I_B = index % b_count;
|
| 67 |
+
|
| 68 |
+
s.K_self = GetMinK(M) + I_K * 2;
|
| 69 |
+
s.K_opp = M - s.K_self;
|
| 70 |
+
|
| 71 |
+
int current_p = 0;
|
| 72 |
+
for (int i = 8; i >= 0; --i) {
|
| 73 |
+
int p = i;
|
| 74 |
+
while (nCr(p + 1, i + 1) <= I_B) p++;
|
| 75 |
+
I_B -= nCr(p, i + 1);
|
| 76 |
+
if (i == 8) s.board[9] = R + 8 - p;
|
| 77 |
+
else s.board[i + 1] = current_p - p - 1;
|
| 78 |
+
current_p = p;
|
| 79 |
+
}
|
| 80 |
+
s.board[0] = current_p;
|
| 81 |
+
return s;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// O(1)-amortized "board odometer": advances a board (sum == R) to the next composition
|
| 85 |
+
// in IndexState (colex combinatorial-number-system) order. Moving a stone leftward is the
|
| 86 |
+
// CNS-rank successor; the inner clear loop is the odometer "carry", amortized O(1) over a
|
| 87 |
+
// full enumeration. Returns false on wrap-around from the last composition (R,0,...,0).
|
| 88 |
+
// Bijection-equivalent to iterating UnindexState(i) for i = 0,1,2,... within a fixed K.
|
| 89 |
+
static inline bool AdvanceBoard(uint8_t b[10]) {
|
| 90 |
+
int S = b[0]; // running prefix sum b[0..j-1]
|
| 91 |
+
for (int j = 1; j <= 9; ++j) {
|
| 92 |
+
if (b[j] > 0) {
|
| 93 |
+
b[j] -= 1;
|
| 94 |
+
for (int t = 0; t <= j - 2; ++t) b[t] = 0;
|
| 95 |
+
b[j - 1] = static_cast<uint8_t>(S + 1);
|
| 96 |
+
return true;
|
| 97 |
+
}
|
| 98 |
+
S += b[j]; // b[j] == 0 here; keeps S == b[0..j]
|
| 99 |
+
}
|
| 100 |
+
return false; // wrapped (last composition reached)
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
// Allocate the static table definition
|
| 105 |
+
inline uint64_t StateIndex::NCR_TABLE[60][10] = {};
|
| 106 |
+
|
| 107 |
+
} // namespace Bestemshe
|
app.py
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bestemshe Perfect-Play Explorer.
|
| 2 |
+
|
| 3 |
+
A custom HTML/JS front end (multilingual, keyboard-driven, rewritable history)
|
| 4 |
+
talks to the C++ tablebase oracle through a hidden Gradio bridge. Keeping the
|
| 5 |
+
UI client-side lets us do global keyboard handling, arrow navigation, clickable
|
| 6 |
+
history and language switching that Gradio's own components cannot express.
|
| 7 |
+
|
| 8 |
+
All user-facing copy, flags and language names live in i18n.py.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import subprocess
|
| 14 |
+
|
| 15 |
+
import gradio as gr
|
| 16 |
+
|
| 17 |
+
from i18n import LANGUAGES, TRANSLATIONS
|
| 18 |
+
|
| 19 |
+
# CPU-only app. If the Space is provisioned on ZeroGPU hardware its startup
|
| 20 |
+
# check demands a @spaces.GPU function; this dummy probe (never called) satisfies
|
| 21 |
+
# it while all real work runs on the always-available CPU.
|
| 22 |
+
try:
|
| 23 |
+
import spaces
|
| 24 |
+
|
| 25 |
+
@spaces.GPU
|
| 26 |
+
def _zerogpu_probe():
|
| 27 |
+
return None
|
| 28 |
+
except ImportError:
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
QUERY_BIN = os.environ.get("BESTEMSHE_QUERY_BIN", "./query")
|
| 32 |
+
DATA_DIR = os.environ.get("BESTEMSHE_DATA_DIR", "layers/compressed")
|
| 33 |
+
TABLEBASE_DATASET = os.environ.get("BESTEMSHE_DATASET", "ansarzeinulla/bestemshe-tablebase")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def ensure_tablebase():
|
| 37 |
+
"""The 8.96GB tablebase lives in a dataset repo (Space repos are capped at 1GB)."""
|
| 38 |
+
if os.path.isdir(DATA_DIR) and any(f.endswith(".bin") for f in os.listdir(DATA_DIR)):
|
| 39 |
+
return
|
| 40 |
+
from huggingface_hub import snapshot_download
|
| 41 |
+
print(f"[setup] downloading tablebase from {TABLEBASE_DATASET} ...")
|
| 42 |
+
snapshot_download(repo_id=TABLEBASE_DATASET, repo_type="dataset", local_dir=".")
|
| 43 |
+
print("[setup] tablebase ready.")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def ensure_query_binary():
|
| 47 |
+
"""On HF Gradio Spaces there is no Docker build step: compile at startup."""
|
| 48 |
+
if os.path.isfile(QUERY_BIN) and os.access(QUERY_BIN, os.X_OK):
|
| 49 |
+
return
|
| 50 |
+
print("[setup] compiling query CLI...")
|
| 51 |
+
subprocess.run(
|
| 52 |
+
["g++", "-std=c++17", "-O3", "-DNDEBUG", "query.cpp", "-o", "query", "-lzstd"],
|
| 53 |
+
check=True,
|
| 54 |
+
)
|
| 55 |
+
print("[setup] query CLI ready.")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def parse_fields(text):
|
| 59 |
+
parts = text.split()
|
| 60 |
+
if len(parts) != 12:
|
| 61 |
+
raise ValueError("Enter 12 numbers.")
|
| 62 |
+
try:
|
| 63 |
+
vals = [int(p) for p in parts]
|
| 64 |
+
except ValueError:
|
| 65 |
+
raise ValueError("All values must be whole numbers.")
|
| 66 |
+
if any(v < 0 or v > 50 for v in vals):
|
| 67 |
+
raise ValueError("Each number must be between 0 and 50.")
|
| 68 |
+
if sum(vals) != 50:
|
| 69 |
+
raise ValueError(f"The stones must total 50 (yours total {sum(vals)}).")
|
| 70 |
+
if vals[0] % 2 or vals[1] % 2:
|
| 71 |
+
raise ValueError("Kazan totals must be even.")
|
| 72 |
+
if vals[0] > 24 or vals[1] > 24:
|
| 73 |
+
raise ValueError("A kazan of 26+ means the game is already over.")
|
| 74 |
+
return vals
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def run_query(state_str):
|
| 78 |
+
env = dict(os.environ, BESTEMSHE_DATA_DIR=DATA_DIR)
|
| 79 |
+
proc = subprocess.run(
|
| 80 |
+
[QUERY_BIN] + state_str.split(),
|
| 81 |
+
capture_output=True, text=True, timeout=60, env=env,
|
| 82 |
+
)
|
| 83 |
+
return json.loads(proc.stdout)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def oracle_bridge(payload):
|
| 87 |
+
"""payload = 'nonce|K1 K2 p0..p9'. Returns JSON string with the same nonce."""
|
| 88 |
+
nonce, _, state = payload.partition("|")
|
| 89 |
+
try:
|
| 90 |
+
parse_fields(state)
|
| 91 |
+
data = run_query(state)
|
| 92 |
+
if "error" in data:
|
| 93 |
+
return json.dumps({"nonce": nonce, "ok": False, "error": data["error"]})
|
| 94 |
+
return json.dumps({"nonce": nonce, "ok": True, "data": data})
|
| 95 |
+
except ValueError as e:
|
| 96 |
+
return json.dumps({"nonce": nonce, "ok": False, "error": str(e)})
|
| 97 |
+
except Exception as e: # noqa: BLE001
|
| 98 |
+
return json.dumps({"nonce": nonce, "ok": False, "error": str(e)})
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# --------------------------------------------------------------------------- #
|
| 102 |
+
# Front end
|
| 103 |
+
# --------------------------------------------------------------------------- #
|
| 104 |
+
LANG_OPTIONS = "".join(
|
| 105 |
+
f'<option value="{l["code"]}">{l["flag"]} {l["name"]}</option>' for l in LANGUAGES
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
INDEX_MARKUP = r"""
|
| 109 |
+
<style>
|
| 110 |
+
#oracle_in, #oracle_out, #oracle_btn { display: none !important; }
|
| 111 |
+
footer { display: none !important; } /* hide Gradio's "Use via API · Built with Gradio" bar */
|
| 112 |
+
.gradio-container { max-width: 100% !important; width: 100% !important; padding: 4px !important; }
|
| 113 |
+
.gradio-container .main, .gradio-container .wrap, .gradio-container .contain { max-width: 100% !important; }
|
| 114 |
+
/* strip Gradio's own horizontal padding so the board can use the full width */
|
| 115 |
+
.fillable, .main.fillable, .app { padding-left: 6px !important; padding-right: 6px !important; }
|
| 116 |
+
.html-container { padding: 0 !important; }
|
| 117 |
+
.block.padded { padding: 0 !important; }
|
| 118 |
+
#bb-wrap { max-width: 1700px; margin: 0 auto; padding: 0 8px;
|
| 119 |
+
font-family: system-ui, sans-serif; color: #2a2118; --bs: 42px; }
|
| 120 |
+
#bb-wrap * { box-sizing: border-box; }
|
| 121 |
+
#bb-cols { display: flex; gap: 16px; align-items: stretch; }
|
| 122 |
+
#bb-main { position: relative; flex: 1 1 auto; min-width: 0; display: flex; align-items: flex-start; }
|
| 123 |
+
#bb-side { flex: 0 0 250px; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
| 124 |
+
#bb-cols.side-hidden #bb-side { display: none; }
|
| 125 |
+
/* collapse / expand the right column */
|
| 126 |
+
#bb-toggle { position: absolute; top: 10px; right: 10px; z-index: 6; width: 34px; height: 34px;
|
| 127 |
+
border-radius: 10px; border: 1px solid #b98f52; background: rgba(255,250,235,0.9);
|
| 128 |
+
color: #6d500e; font-size: 18px; font-weight: 800; cursor: pointer; line-height: 1;
|
| 129 |
+
display: flex; align-items: center; justify-content: center; }
|
| 130 |
+
#bb-toggle:hover { background: #fff4d6; }
|
| 131 |
+
@media (max-width: 860px) {
|
| 132 |
+
#bb-wrap { padding: 0 6px; }
|
| 133 |
+
#bb-cols { flex-direction: column; gap: 12px; }
|
| 134 |
+
#bb-side { flex: 1 1 auto; width: 100%; }
|
| 135 |
+
.bb-board { padding: 10px; border-radius: 18px; gap: 6px; }
|
| 136 |
+
.bb-cellrow, .bb-numrow { gap: 5px; }
|
| 137 |
+
.bb-cell { padding: 4px; border-radius: 9px; }
|
| 138 |
+
.bb-numrow .n { font-size: 14px; }
|
| 139 |
+
.bb-kazan { padding: 6px 10px; gap: 8px; }
|
| 140 |
+
.bb-kcount { font-size: 18px; }
|
| 141 |
+
.bb-hist { min-height: 220px; }
|
| 142 |
+
#bb-toggle { top: 6px; right: 6px; }
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.bb-card { background: #fffdf5; border: 1px solid #ecdfb8; border-radius: 16px; padding: 14px;
|
| 146 |
+
display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
|
| 147 |
+
|
| 148 |
+
/* ---- board: a real Bestemshe board with kumalak stones ---- */
|
| 149 |
+
.bb-board { flex: 1 1 auto; width: 100%;
|
| 150 |
+
background: linear-gradient(160deg, #ddba86 0%, #c99f68 100%);
|
| 151 |
+
border: 2px solid #a67c48; border-radius: 22px; padding: 16px;
|
| 152 |
+
box-shadow: inset 0 2px 10px rgba(120,80,30,0.20), 0 6px 18px rgba(120,80,20,0.16);
|
| 153 |
+
display: flex; flex-direction: column; gap: 8px; }
|
| 154 |
+
|
| 155 |
+
.ball { position: relative; display: inline-block; width: var(--bs); height: var(--bs); border-radius: 50%;
|
| 156 |
+
background: radial-gradient(circle at 34% 30%, #fffdf6 0%, #eed9a6 46%, #bd9051 100%);
|
| 157 |
+
box-shadow: inset 0 -2px 4px rgba(90,60,20,0.40), 0 1px 2px rgba(0,0,0,0.28); }
|
| 158 |
+
.ball.ghost { background: none; box-shadow: none; }
|
| 159 |
+
/* stones inside the play cells sit a touch smaller than their slot for breathing room */
|
| 160 |
+
.bb-cell .ball { transform: scale(0.9); }
|
| 161 |
+
/* a stacked (overlaid) layer of stones is shown as a dark dot at the centre */
|
| 162 |
+
.ball .ov { position: absolute; top: 25%; left: 25%; width: 50%; height: 50%; border-radius: 50%;
|
| 163 |
+
background: #241a0d; box-shadow: inset 0 1px 1px rgba(255,255,255,0.18); }
|
| 164 |
+
|
| 165 |
+
/* kazans (stores) — lighter, no name labels */
|
| 166 |
+
.bb-kazan { --ks: clamp(16px, calc(var(--bs) * 0.58), 32px); /* kazan stones stay compact */
|
| 167 |
+
display: flex; align-items: center; gap: 12px; padding: 8px 14px; border-radius: 16px;
|
| 168 |
+
background: rgba(255,250,235,0.28); border: 2px solid rgba(120,80,30,0.28);
|
| 169 |
+
height: calc(var(--ks) * 2 + 20px); /* FIXED — never changes with stone count */
|
| 170 |
+
flex: 0 0 auto; transition: box-shadow .15s, border-color .15s; }
|
| 171 |
+
.bb-kazan.turn { border-color: #d99a1f; box-shadow: 0 0 0 1px rgba(217,154,31,0.5), 0 0 14px rgba(217,154,31,0.35); }
|
| 172 |
+
.bb-kcount { font-weight: 800; font-size: 22px; color: #5a3d18; min-width: 34px; text-align: center;
|
| 173 |
+
background: rgba(120,80,30,0.14); border-radius: 12px; padding: 3px 10px;
|
| 174 |
+
font-variant-numeric: tabular-nums; }
|
| 175 |
+
.bb-kballs { display: flex; flex-wrap: nowrap; align-items: center; gap: 4px; flex: 1 1 auto;
|
| 176 |
+
overflow: hidden; } /* one row of columns only — kazan height stays fixed */
|
| 177 |
+
.bb-kballs .ball { width: var(--ks); height: var(--ks); }
|
| 178 |
+
.bb-kcol { display: flex; flex-direction: column; gap: 4px; height: calc(var(--ks) * 2 + 4px); }
|
| 179 |
+
.bb-kazan.kazan-black .bb-kcol { justify-content: flex-start; } /* gravity top */
|
| 180 |
+
.bb-kazan.kazan-white .bb-kcol { justify-content: flex-end; } /* gravity down */
|
| 181 |
+
|
| 182 |
+
/* count sub-rows between cells and kazans */
|
| 183 |
+
.bb-numrow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; padding: 0 2px; }
|
| 184 |
+
.bb-numrow .n { text-align: center; font-weight: 800; font-size: 17px; color: #5a3d18;
|
| 185 |
+
font-variant-numeric: tabular-nums; }
|
| 186 |
+
|
| 187 |
+
/* the five cells of each side — each exactly 2 stones wide, 5 tall */
|
| 188 |
+
.bb-cellrow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
|
| 189 |
+
.bb-board { overflow-x: hidden; }
|
| 190 |
+
.bb-cell { border-radius: 12px; border: 2px solid rgba(90,60,25,0.30);
|
| 191 |
+
background: rgba(70,45,18,0.16); padding: 6px;
|
| 192 |
+
height: calc(var(--bs) * 5 + 36px); display: flex;
|
| 193 |
+
transition: box-shadow .15s, border-color .15s, background .12s; }
|
| 194 |
+
.bb-cell.mv { cursor: pointer; }
|
| 195 |
+
.bb-cell.mv:hover { background: rgba(70,45,18,0.26); }
|
| 196 |
+
.bb-cell.WIN { border-color: #2fa740; box-shadow: 0 0 0 1px rgba(47,167,64,0.6), 0 0 14px rgba(47,167,64,0.55); }
|
| 197 |
+
.bb-cell.DRAW { border-color: #e8c400; box-shadow: 0 0 0 1px rgba(232,196,0,0.65), 0 0 14px rgba(232,196,0,0.6); }
|
| 198 |
+
.bb-cell.LOSS { border-color: #e23b3b; box-shadow: 0 0 0 1px rgba(226,59,59,0.6), 0 0 14px rgba(226,59,59,0.55); }
|
| 199 |
+
.bb-stack { display: flex; flex-direction: column; gap: 6px; flex: 1; }
|
| 200 |
+
.bb-cellrow.row-black .bb-stack { justify-content: flex-start; } /* grows downward */
|
| 201 |
+
.bb-cellrow.row-white .bb-stack { justify-content: flex-end; } /* grows upward */
|
| 202 |
+
.bb-brow { display: flex; gap: 6px; justify-content: center; } /* 2 columns, left gravity */
|
| 203 |
+
|
| 204 |
+
.bb-side-row { display: flex; gap: 8px; align-items: center; }
|
| 205 |
+
#bb-langsel { width: 100%; font-size: 15px; padding: 9px 10px; border-radius: 12px;
|
| 206 |
+
border: 1px solid #e2c98a; background: #fffdf5; color: #2a2118; cursor: pointer; }
|
| 207 |
+
.bb-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
| 208 |
+
.bb-icon { cursor: pointer; border: 1px solid #e2c98a; background: #fff8e6; border-radius: 12px;
|
| 209 |
+
height: 46px; font-size: 22px; font-weight: 800; color: #8a5a00; padding: 0;
|
| 210 |
+
line-height: 1; display: flex; align-items: center; justify-content: center;
|
| 211 |
+
font-family: system-ui, sans-serif; }
|
| 212 |
+
.bb-icon:hover { background: #ffedc2; color: #5c3c00; }
|
| 213 |
+
#bb-btn-setup { font-size: 27px; } /* the gear reads optically small — nudge it up */
|
| 214 |
+
|
| 215 |
+
.bb-hist-title { font-weight: 700; color: #6d500e; margin: 0 0 8px; font-size: 14px;
|
| 216 |
+
text-transform: uppercase; letter-spacing: .04em; }
|
| 217 |
+
.bb-hist { font-family: ui-monospace, monospace; font-size: 15px;
|
| 218 |
+
flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
|
| 219 |
+
.bb-hrow { display: grid; grid-template-columns: 34px 1fr 1fr; gap: 4px; align-items: center;
|
| 220 |
+
line-height: 1.5; padding: 2px 0; }
|
| 221 |
+
.bb-turn { color: #b3a276; text-align: right; padding-right: 4px; }
|
| 222 |
+
.bb-mv { cursor: pointer; padding: 3px 8px; border-radius: 7px; text-align: center; }
|
| 223 |
+
.bb-mv:hover { background: #ffedc2; }
|
| 224 |
+
.bb-mv.on { background: #d9822b; color: #fff; }
|
| 225 |
+
|
| 226 |
+
.bb-overlay { position: fixed; inset: 0; background: rgba(30,22,8,0.5); display: none;
|
| 227 |
+
align-items: center; justify-content: center; z-index: 1000; }
|
| 228 |
+
.bb-overlay.open { display: flex; }
|
| 229 |
+
.bb-modal { background: #fffdf8; border-radius: 18px; padding: 24px; max-width: 70%; width: 100%;
|
| 230 |
+
max-height: 88vh; overflow: auto; box-shadow: 0 14px 48px rgba(0,0,0,0.35); }
|
| 231 |
+
.bb-modal h3 { margin: 0 0 14px; color: #6d500e; }
|
| 232 |
+
.bb-modal textarea { width: 100%; font-family: ui-monospace, monospace; font-size: 15px;
|
| 233 |
+
padding: 10px; border: 1px solid #d8c692; border-radius: 10px; background: #fff; }
|
| 234 |
+
.bb-modal .hint { font-size: 15.5px; color: #4c4130; margin: 10px 0 4px; line-height: 1.75; }
|
| 235 |
+
#bb-fen-help { background: #fbf3dc; border: 1px solid #ecdfb8; border-radius: 10px;
|
| 236 |
+
padding: 12px 14px; margin-top: 12px; }
|
| 237 |
+
#bb-fen-help b { color: #6d500e; }
|
| 238 |
+
.bb-modal .hint b { color: #6d500e; }
|
| 239 |
+
.bb-modal .keys { display: grid; grid-template-columns: 74px 1fr; gap: 6px 12px;
|
| 240 |
+
font-size: 14.5px; color: #4c4130; margin: 10px 0; align-items: center; }
|
| 241 |
+
.bb-modal .keys kbd { background: #f1e5c0; border: 1px solid #d8c692; border-radius: 6px;
|
| 242 |
+
padding: 2px 8px; font-family: ui-monospace, monospace; font-size: 13px;
|
| 243 |
+
text-align: center; }
|
| 244 |
+
.bb-modal .err { color: #c62828; font-size: 14px; margin-top: 8px; min-height: 18px; }
|
| 245 |
+
.bb-modal .credit { font-size: 13px; color: #6d500e; margin-top: 14px; line-height: 1.6;
|
| 246 |
+
border-top: 1px solid #ecdfb8; padding-top: 12px; }
|
| 247 |
+
.bb-modal .btns { display: flex; gap: 10px; justify-content: stretch; margin-top: 16px; }
|
| 248 |
+
/* Start and Close buttons are forced to identical size/height. */
|
| 249 |
+
.bb-modal .btns button { flex: 1 1 0; cursor: pointer; border-radius: 10px; padding: 0;
|
| 250 |
+
height: 46px; line-height: 46px; box-sizing: border-box;
|
| 251 |
+
display: flex; align-items: center; justify-content: center;
|
| 252 |
+
font-weight: 700; font-size: 15px; border: 1px solid #d9822b;
|
| 253 |
+
background: #d9822b; color: #fff; }
|
| 254 |
+
.bb-modal .btns button:hover { background: #c4741f; }
|
| 255 |
+
.bb-end-title { text-align: center; font-size: 22px; }
|
| 256 |
+
</style>
|
| 257 |
+
<div id="bb-wrap">
|
| 258 |
+
<div id="bb-cols">
|
| 259 |
+
<div id="bb-main">
|
| 260 |
+
<button id="bb-toggle" title="Hide / show panel">⇥</button>
|
| 261 |
+
<div class="bb-board" id="bb-board"></div>
|
| 262 |
+
</div>
|
| 263 |
+
<div id="bb-side">
|
| 264 |
+
<select id="bb-langsel">__LANG_OPTIONS__</select>
|
| 265 |
+
<div class="bb-actions">
|
| 266 |
+
<button class="bb-icon" data-act="new" id="bb-btn-new">↺</button>
|
| 267 |
+
<button class="bb-icon" data-act="open" data-modal="bb-setup" id="bb-btn-setup">⚙</button>
|
| 268 |
+
<button class="bb-icon" data-act="open" data-modal="bb-help" id="bb-btn-help">?</button>
|
| 269 |
+
<button class="bb-icon" data-act="pgn" id="bb-btn-pgn">⤓</button>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="bb-card">
|
| 272 |
+
<div class="bb-hist-title" id="bb-hist-title"></div>
|
| 273 |
+
<div class="bb-hist" id="bb-hist"></div>
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
<div class="bb-overlay" id="bb-setup"><div class="bb-modal" style="max-width: 50%">
|
| 279 |
+
<h3 id="bb-setup-title"></h3>
|
| 280 |
+
<textarea id="bb-fen" rows="2"></textarea>
|
| 281 |
+
<div class="hint" id="bb-fen-help"></div>
|
| 282 |
+
<div class="err" id="bb-setup-err"></div>
|
| 283 |
+
<div class="btns"><button data-act="close" data-modal="bb-setup" id="bb-setup-close"></button>
|
| 284 |
+
<button data-act="apply" id="bb-setup-apply"></button></div>
|
| 285 |
+
</div></div>
|
| 286 |
+
<div class="bb-overlay" id="bb-help"><div class="bb-modal">
|
| 287 |
+
<h3 id="bb-help-title"></h3>
|
| 288 |
+
<div class="hint" id="bb-help-intro"></div>
|
| 289 |
+
<div class="keys" id="bb-help-keys"></div>
|
| 290 |
+
<div class="hint" id="bb-help-extra"></div>
|
| 291 |
+
<div class="credit" id="bb-help-credit"></div>
|
| 292 |
+
<div class="btns"><button data-act="close" data-modal="bb-help" id="bb-help-close"></button></div>
|
| 293 |
+
</div></div>
|
| 294 |
+
<div class="bb-overlay" id="bb-end"><div class="bb-modal">
|
| 295 |
+
<h3 class="bb-end-title" id="bb-end-title"></h3>
|
| 296 |
+
<div class="hint" id="bb-end-msg" style="text-align:center"></div>
|
| 297 |
+
<div class="btns"><button data-act="close" data-modal="bb-end" id="bb-end-close"></button>
|
| 298 |
+
<button data-act="pgn" id="bb-end-pgn"></button></div>
|
| 299 |
+
</div></div>
|
| 300 |
+
</div>
|
| 301 |
+
""".replace("__LANG_OPTIONS__", LANG_OPTIONS)
|
| 302 |
+
|
| 303 |
+
INDEX_JS = r"""
|
| 304 |
+
() => {
|
| 305 |
+
if (window.__bbInit) return; window.__bbInit = true;
|
| 306 |
+
const DEFAULT_FEN = "5,5,5,5,5/5,5,5,5,5 0,0 w 1";
|
| 307 |
+
|
| 308 |
+
const I18N = __I18N_JSON__;
|
| 309 |
+
const SITE = "https://huggingface.co/spaces/ansarzeinulla/Bestemshe-God-Algorithm";
|
| 310 |
+
|
| 311 |
+
let lang = "EN";
|
| 312 |
+
let initialFen = DEFAULT_FEN;
|
| 313 |
+
let line = []; // nodes {state, ply, moveIn, key, moves, gameOver, winner}
|
| 314 |
+
let played = []; // notations, aligned to line transitions
|
| 315 |
+
let cursor = 0;
|
| 316 |
+
let busy = false;
|
| 317 |
+
const t = (k) => (I18N[lang][k] !== undefined ? I18N[lang][k] : I18N.EN[k]);
|
| 318 |
+
// Two roles by side index: 0 = the first player (moves first), 1 = the follower.
|
| 319 |
+
// Names are translated, so winners are tracked by side index, not by name.
|
| 320 |
+
const nameFor = (side) => (side === 0 ? t("nameFirst") : t("nameSecond"));
|
| 321 |
+
|
| 322 |
+
// ---- FEN <-> internal state -------------------------------------------- #
|
| 323 |
+
// Internal state is mover-relative "K1 K2 p0..p9" (K1/p0-4 = side to move).
|
| 324 |
+
// FEN is colour-absolute: w1..w5/b1..b5 wKazan,bKazan side moveNo (White=Bastaushi).
|
| 325 |
+
function absView(state, ply) {
|
| 326 |
+
const v = state.split(" ").map(Number);
|
| 327 |
+
const kM = v[0], kO = v[1], b = v.slice(2);
|
| 328 |
+
if (ply % 2 === 0) return { bk: kM, kk: kO, bp: b.slice(0, 5), kp: b.slice(5, 10), moverBottom: true };
|
| 329 |
+
return { bk: kO, kk: kM, bp: b.slice(5, 10), kp: b.slice(0, 5), moverBottom: false };
|
| 330 |
+
}
|
| 331 |
+
function nodeToFen(node) {
|
| 332 |
+
const av = absView(node.state, node.ply);
|
| 333 |
+
const side = node.ply % 2 === 0 ? "w" : "b";
|
| 334 |
+
const moveNo = Math.floor(node.ply / 2) + 1;
|
| 335 |
+
return av.bp.join(",") + "/" + av.kp.join(",") + " " +
|
| 336 |
+
av.bk + "," + av.kk + " " + side + " " + moveNo;
|
| 337 |
+
}
|
| 338 |
+
// Returns {state, ply} or {error}.
|
| 339 |
+
function fenToInternal(fen) {
|
| 340 |
+
try {
|
| 341 |
+
const parts = fen.trim().split(/\s+/);
|
| 342 |
+
if (parts.length < 3) return { error: "bad" };
|
| 343 |
+
const cells = parts[0].split("/");
|
| 344 |
+
if (cells.length !== 2) return { error: "bad" };
|
| 345 |
+
const white = cells[0].split(",").map(Number);
|
| 346 |
+
const black = cells[1].split(",").map(Number);
|
| 347 |
+
const kaz = parts[1].split(",").map(Number);
|
| 348 |
+
if (white.length !== 5 || black.length !== 5 || kaz.length !== 2) return { error: "bad" };
|
| 349 |
+
const all = white.concat(black, kaz);
|
| 350 |
+
if (all.some((n) => !Number.isInteger(n) || n < 0)) return { error: "bad" };
|
| 351 |
+
const side = (parts[2] || "w").toLowerCase();
|
| 352 |
+
const moveNo = parts[3] !== undefined ? parseInt(parts[3], 10) : 1;
|
| 353 |
+
const wk = kaz[0], bk = kaz[1];
|
| 354 |
+
let state, ply;
|
| 355 |
+
if (side === "b") { // Black (Kostaushi) to move
|
| 356 |
+
state = bk + " " + wk + " " + black.concat(white).join(" ");
|
| 357 |
+
ply = (Math.max(1, moveNo) - 1) * 2 + 1;
|
| 358 |
+
} else { // White (Bastaushi) to move
|
| 359 |
+
state = wk + " " + bk + " " + white.concat(black).join(" ");
|
| 360 |
+
ply = (Math.max(1, moveNo) - 1) * 2;
|
| 361 |
+
}
|
| 362 |
+
return { state, ply };
|
| 363 |
+
} catch (e) { return { error: "bad" }; }
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// ---- oracle bridge ----------------------------------------------------- #
|
| 367 |
+
function setNative(el, val) {
|
| 368 |
+
const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
|
| 369 |
+
set.call(el, val); el.dispatchEvent(new Event("input", { bubbles: true }));
|
| 370 |
+
}
|
| 371 |
+
function oracleOnce(state) {
|
| 372 |
+
return new Promise((resolve, reject) => {
|
| 373 |
+
const nonce = Math.random().toString(36).slice(2);
|
| 374 |
+
const inp = document.querySelector("#oracle_in textarea");
|
| 375 |
+
const out = document.querySelector("#oracle_out textarea");
|
| 376 |
+
const btn = document.querySelector("#oracle_btn button") || document.querySelector("#oracle_btn");
|
| 377 |
+
if (!inp || !out || !btn) { reject("bridge not ready"); return; }
|
| 378 |
+
let done = false;
|
| 379 |
+
const poll = setInterval(() => {
|
| 380 |
+
try { const j = JSON.parse(out.value); if (j.nonce === nonce) { done = true; clearInterval(poll); resolve(j); } } catch (e) {}
|
| 381 |
+
}, 70);
|
| 382 |
+
setTimeout(() => { if (!done) { clearInterval(poll); reject("timeout"); } }, 6000);
|
| 383 |
+
setNative(inp, nonce + "|" + state);
|
| 384 |
+
btn.click();
|
| 385 |
+
});
|
| 386 |
+
}
|
| 387 |
+
async function oracle(state) {
|
| 388 |
+
let lastErr;
|
| 389 |
+
for (let attempt = 0; attempt < 4; attempt++) {
|
| 390 |
+
try { return await oracleOnce(state); }
|
| 391 |
+
catch (e) { lastErr = e; await new Promise((r) => setTimeout(r, 250)); }
|
| 392 |
+
}
|
| 393 |
+
throw lastErr;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
async function makeNode(state, ply, moveIn) {
|
| 397 |
+
const node = { state, ply, moveIn, key: (ply % 2) + ":" + state, moves: [], gameOver: null, winner: null };
|
| 398 |
+
const res = await oracle(state);
|
| 399 |
+
if (!res.ok) { node.error = res.error; return node; }
|
| 400 |
+
node.moves = res.data.moves;
|
| 401 |
+
if (node.moves.length === 0) { node.gameOver = "nomove"; node.winner = (ply + 1) % 2; }
|
| 402 |
+
return node;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
async function newGame(fen) {
|
| 406 |
+
const parsed = fenToInternal(fen);
|
| 407 |
+
if (parsed.error) return "bad";
|
| 408 |
+
const n0 = await makeNode(parsed.state, parsed.ply, null);
|
| 409 |
+
if (n0.error) return n0.error;
|
| 410 |
+
initialFen = fen; line = [n0]; played = []; cursor = 0; render();
|
| 411 |
+
return null;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
async function playMove(node, m) {
|
| 415 |
+
if (busy) return; busy = true;
|
| 416 |
+
try {
|
| 417 |
+
line = line.slice(0, cursor + 1);
|
| 418 |
+
played = played.slice(0, cursor);
|
| 419 |
+
const notation = "" + m.from + m.to + (m.capture ? "+" : "");
|
| 420 |
+
const cs = m.child.K1 + " " + m.child.K2 + " " + m.child.board.join(" ");
|
| 421 |
+
const cp = node.ply + 1;
|
| 422 |
+
let child;
|
| 423 |
+
if (m.terminal) {
|
| 424 |
+
child = { state: cs, ply: cp, moveIn: notation, key: (cp % 2) + ":" + cs,
|
| 425 |
+
moves: [], gameOver: "win", winner: node.ply % 2 };
|
| 426 |
+
} else {
|
| 427 |
+
// Loop detection: we keep every position (side-to-move + board) of the
|
| 428 |
+
// current line; if this child repeats one of them, it is an infinite-loop draw.
|
| 429 |
+
const repeat = line.some((nd) => nd.key === (cp % 2) + ":" + cs);
|
| 430 |
+
child = await makeNode(cs, cp, notation);
|
| 431 |
+
child.moveIn = notation;
|
| 432 |
+
if (repeat) { child.gameOver = "draw"; child.winner = null; child.moves = []; }
|
| 433 |
+
}
|
| 434 |
+
line.push(child); played.push(notation); cursor = line.length - 1; render();
|
| 435 |
+
if (child.gameOver) announceEnd(child);
|
| 436 |
+
} finally { busy = false; }
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function optimal(node) {
|
| 440 |
+
const rank = { WIN: 2, DRAW: 1, LOSS: 0 };
|
| 441 |
+
let best = -1; node.moves.forEach((m) => { best = Math.max(best, rank[m.result]); });
|
| 442 |
+
return node.moves.filter((m) => rank[m.result] === best);
|
| 443 |
+
}
|
| 444 |
+
async function inputIndex(i) {
|
| 445 |
+
const node = line[cursor];
|
| 446 |
+
if (!node || node.gameOver || node.error) return;
|
| 447 |
+
if (i === 0) { // random OPTIMAL move
|
| 448 |
+
const b = optimal(node);
|
| 449 |
+
if (b.length) await playMove(node, b[Math.floor(Math.random() * b.length)]);
|
| 450 |
+
return;
|
| 451 |
+
}
|
| 452 |
+
if (i === 9) { // random move — optimality ignored
|
| 453 |
+
if (node.moves.length) await playMove(node, node.moves[Math.floor(Math.random() * node.moves.length)]);
|
| 454 |
+
return;
|
| 455 |
+
}
|
| 456 |
+
const m = node.moves.find((x) => x.from === i);
|
| 457 |
+
if (m) await playMove(node, m);
|
| 458 |
+
}
|
| 459 |
+
function go(idx) { cursor = Math.max(0, Math.min(line.length - 1, idx)); render(); }
|
| 460 |
+
|
| 461 |
+
// ---- PGN --------------------------------------------------------------- #
|
| 462 |
+
function resultToken() {
|
| 463 |
+
const last = line[line.length - 1];
|
| 464 |
+
if (last.gameOver === "draw") return "1/2-1/2";
|
| 465 |
+
if (last.gameOver === "win" || last.gameOver === "nomove")
|
| 466 |
+
return last.winner === 0 ? "1-0" : "0-1";
|
| 467 |
+
return "*";
|
| 468 |
+
}
|
| 469 |
+
function pgnMoves() {
|
| 470 |
+
let out = [];
|
| 471 |
+
for (let i = 0; i < played.length; i += 2) {
|
| 472 |
+
out.push((i / 2 + 1) + ". " + played.slice(i, i + 2).join(" "));
|
| 473 |
+
}
|
| 474 |
+
let body = out.join(" ");
|
| 475 |
+
const last = line[line.length - 1];
|
| 476 |
+
if (last.gameOver === "draw") body += " {Loop is reached}";
|
| 477 |
+
const res = resultToken();
|
| 478 |
+
if (res !== "*") body += " " + res;
|
| 479 |
+
return body;
|
| 480 |
+
}
|
| 481 |
+
function downloadPgn() {
|
| 482 |
+
const d = new Date();
|
| 483 |
+
const date = d.getFullYear() + "." +
|
| 484 |
+
String(d.getMonth() + 1).padStart(2, "0") + "." + String(d.getDate()).padStart(2, "0");
|
| 485 |
+
const header =
|
| 486 |
+
'[Event "Single Play Versus God"]\n' +
|
| 487 |
+
'[Site "' + SITE + '"]\n' +
|
| 488 |
+
'[Date "' + date + '"]\n' +
|
| 489 |
+
'[White "Player"]\n' +
|
| 490 |
+
'[Black "God"]\n' +
|
| 491 |
+
'[Result "' + resultToken() + '"]\n' +
|
| 492 |
+
'[PlyCount "' + played.length + '"]\n' +
|
| 493 |
+
'[Annotator "God"]\n' +
|
| 494 |
+
'[Mode "online"]\n';
|
| 495 |
+
const pgn = header + "\n" + pgnMoves() + "\n";
|
| 496 |
+
const a = document.createElement("a");
|
| 497 |
+
a.href = URL.createObjectURL(new Blob([pgn], { type: "text/plain" }));
|
| 498 |
+
a.download = "bestemshe.pgn"; a.click();
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// ---- rendering --------------------------------------------------------- #
|
| 502 |
+
// Colour-absolute view of a node (White = Bastaushi, Black = Kostaushi).
|
| 503 |
+
function colorView(node) {
|
| 504 |
+
const v = node.state.split(" ").map(Number);
|
| 505 |
+
const k1 = v[0], k2 = v[1], p = v.slice(2);
|
| 506 |
+
const whiteToMove = node.ply % 2 === 0;
|
| 507 |
+
let wk, bk, wp, bp;
|
| 508 |
+
if (whiteToMove) { wk = k1; bk = k2; wp = p.slice(0, 5); bp = p.slice(5, 10); }
|
| 509 |
+
else { bk = k1; wk = k2; bp = p.slice(0, 5); wp = p.slice(5, 10); }
|
| 510 |
+
return { wk, bk, wp, bp, whiteToMove };
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// A cell is exactly 2 stones wide and 5 stones tall — 10 visible slots.
|
| 514 |
+
// Stones fill row by row, left column first (an odd stone sits on the left).
|
| 515 |
+
// `side` sets the growth direction: black grows downward (rows top→bottom),
|
| 516 |
+
// white grows upward (rows bottom→top). Once a slot is full, further stones
|
| 517 |
+
// stack a new layer on top of it — shown as a small dark dot in the stone's
|
| 518 |
+
// centre rather than a second circle. Slot i holds ceil((n-i)/10) layers.
|
| 519 |
+
function cellStack(n, side) {
|
| 520 |
+
let rows = [];
|
| 521 |
+
for (let r = 0; r < 5; r++) {
|
| 522 |
+
let s = "";
|
| 523 |
+
for (let c = 0; c < 2; c++) {
|
| 524 |
+
const i = r * 2 + c;
|
| 525 |
+
const layers = n > i ? Math.ceil((n - i) / 10) : 0;
|
| 526 |
+
if (layers <= 0) s += '<span class="ball ghost"></span>';
|
| 527 |
+
else s += '<span class="ball">' + (layers >= 2 ? '<i class="ov"></i>' : "") + "</span>";
|
| 528 |
+
}
|
| 529 |
+
rows.push('<div class="bb-brow">' + s + "</div>");
|
| 530 |
+
}
|
| 531 |
+
if (side === "white") rows.reverse();
|
| 532 |
+
return '<div class="bb-stack">' + rows.join("") + "</div>";
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
// A kazan holds stones in columns of 2, filled column by column (left→right).
|
| 536 |
+
// Vertical gravity (top for black, bottom for white) is handled in CSS.
|
| 537 |
+
function kazanStack(n) {
|
| 538 |
+
let cols = [];
|
| 539 |
+
const ncols = Math.ceil(n / 2);
|
| 540 |
+
for (let c = 0; c < ncols; c++) {
|
| 541 |
+
const inCol = Math.min(2, n - c * 2);
|
| 542 |
+
let b = "";
|
| 543 |
+
for (let k = 0; k < inCol; k++) b += '<span class="ball"></span>';
|
| 544 |
+
cols.push('<div class="bb-kcol">' + b + "</div>");
|
| 545 |
+
}
|
| 546 |
+
return cols.join("");
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
function cellHtml(count, pit, isMover, move, side) {
|
| 550 |
+
const cls = "bb-cell" + (isMover && move ? " mv " + move.result : "");
|
| 551 |
+
const data = isMover && move ? ' data-act="pit" data-i="' + pit + '"' : "";
|
| 552 |
+
return '<div class="' + cls + '"' + data + ">" + cellStack(count, side) + "</div>";
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
// Each cell is 2 stones wide; the five cells fill the board's width, so the
|
| 556 |
+
// stone size is whatever makes 2 of them fit snugly inside one cell. Recomputed
|
| 557 |
+
// on every render and on resize (keeps stones as large as the space allows).
|
| 558 |
+
function sizeBoard() {
|
| 559 |
+
const board = document.getElementById("bb-board");
|
| 560 |
+
const wrap = document.getElementById("bb-wrap");
|
| 561 |
+
if (!board || !wrap) return;
|
| 562 |
+
const cs = getComputedStyle(board);
|
| 563 |
+
const inner = board.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
| 564 |
+
const cellGap = 8, cellPad = 6, colGap = 6; // must match the CSS
|
| 565 |
+
const cellOuter = (inner - 4 * cellGap) / 5;
|
| 566 |
+
let bs = Math.floor((cellOuter - 2 * cellPad - colGap) / 2); // largest that fits the width
|
| 567 |
+
bs = Math.max(14, Math.min(120, bs));
|
| 568 |
+
wrap.style.setProperty("--bs", bs + "px");
|
| 569 |
+
// Then measure the real board height and shrink until it fits the device viewport.
|
| 570 |
+
for (let i = 0; i < 5; i++) {
|
| 571 |
+
const avail = window.innerHeight - board.getBoundingClientRect().top - 12;
|
| 572 |
+
const h = board.offsetHeight;
|
| 573 |
+
if (h <= avail || bs <= 14) break;
|
| 574 |
+
bs = Math.max(14, Math.floor(bs * avail / h));
|
| 575 |
+
wrap.style.setProperty("--bs", bs + "px");
|
| 576 |
+
}
|
| 577 |
+
}
|
| 578 |
+
window.addEventListener("resize", sizeBoard);
|
| 579 |
+
|
| 580 |
+
function announceEnd(node) {
|
| 581 |
+
const titleEl = document.getElementById("bb-end-title");
|
| 582 |
+
const msgEl = document.getElementById("bb-end-msg");
|
| 583 |
+
if (node.gameOver === "draw") {
|
| 584 |
+
titleEl.textContent = "🔁 " + t("drawLoop");
|
| 585 |
+
msgEl.textContent = t("loopHint");
|
| 586 |
+
} else {
|
| 587 |
+
titleEl.textContent = "🏆 " + nameFor(node.winner) + " " + t("wins");
|
| 588 |
+
msgEl.textContent = "";
|
| 589 |
+
}
|
| 590 |
+
document.getElementById("bb-end").classList.add("open");
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
function render() {
|
| 594 |
+
const node = line[cursor];
|
| 595 |
+
const cv = colorView(node);
|
| 596 |
+
const moverMoves = new Map();
|
| 597 |
+
if (!node.gameOver && !node.error) node.moves.forEach((m) => moverMoves.set(m.from, m));
|
| 598 |
+
|
| 599 |
+
// Fixed board: Black (Kostaushi) on top, White (Bastaushi) on the bottom.
|
| 600 |
+
// Each player numbers pits 1..5 from their own left, so Black's pits read
|
| 601 |
+
// right-to-left across the top; White's read left-to-right along the bottom.
|
| 602 |
+
let blackCells = "", blackNums = "", whiteCells = "", whiteNums = "";
|
| 603 |
+
for (let c = 1; c <= 5; c++) {
|
| 604 |
+
const bp = 6 - c; // black pit at visual column c
|
| 605 |
+
const bMover = !cv.whiteToMove;
|
| 606 |
+
blackCells += cellHtml(cv.bp[bp - 1], bp, bMover, moverMoves.get(bp), "black");
|
| 607 |
+
blackNums += '<div class="n">' + cv.bp[bp - 1] + "</div>";
|
| 608 |
+
const wp = c; // white pit at visual column c
|
| 609 |
+
const wMover = cv.whiteToMove;
|
| 610 |
+
whiteCells += cellHtml(cv.wp[wp - 1], wp, wMover, moverMoves.get(wp), "white");
|
| 611 |
+
whiteNums += '<div class="n">' + cv.wp[wp - 1] + "</div>";
|
| 612 |
+
}
|
| 613 |
+
const blackTurn = cv.whiteToMove ? "" : " turn";
|
| 614 |
+
const whiteTurn = cv.whiteToMove ? " turn" : "";
|
| 615 |
+
|
| 616 |
+
document.getElementById("bb-board").innerHTML =
|
| 617 |
+
'<div class="bb-kazan kazan-black' + blackTurn + '">' +
|
| 618 |
+
'<span class="bb-kcount">' + cv.bk + '</span>' +
|
| 619 |
+
'<div class="bb-kballs">' + kazanStack(cv.bk) + "</div></div>" +
|
| 620 |
+
'<div class="bb-numrow">' + blackNums + "</div>" +
|
| 621 |
+
'<div class="bb-cellrow row-black">' + blackCells + "</div>" +
|
| 622 |
+
'<div class="bb-cellrow row-white">' + whiteCells + "</div>" +
|
| 623 |
+
'<div class="bb-numrow">' + whiteNums + "</div>" +
|
| 624 |
+
'<div class="bb-kazan kazan-white' + whiteTurn + '">' +
|
| 625 |
+
'<span class="bb-kcount">' + cv.wk + '</span>' +
|
| 626 |
+
'<div class="bb-kballs">' + kazanStack(cv.wk) + "</div></div>";
|
| 627 |
+
sizeBoard();
|
| 628 |
+
|
| 629 |
+
let hist = "";
|
| 630 |
+
for (let i = 0; i < played.length; i += 2) {
|
| 631 |
+
hist += '<div class="bb-hrow"><span class="bb-turn">' + (i / 2 + 1) + ".</span>";
|
| 632 |
+
for (let j = i; j < i + 2; j++) {
|
| 633 |
+
hist += (j < played.length)
|
| 634 |
+
? '<span class="bb-mv' + (j + 1 === cursor ? " on" : "") + '" data-act="hist" data-idx="' + j + '">' + played[j] + "</span>"
|
| 635 |
+
: "<span></span>";
|
| 636 |
+
}
|
| 637 |
+
hist += "</div>";
|
| 638 |
+
}
|
| 639 |
+
const histEl = document.getElementById("bb-hist");
|
| 640 |
+
histEl.innerHTML = hist;
|
| 641 |
+
document.getElementById("bb-hist-title").textContent = t("history");
|
| 642 |
+
const onEl = histEl.querySelector(".bb-mv.on");
|
| 643 |
+
if (onEl) onEl.scrollIntoView({ block: "nearest" });
|
| 644 |
+
else histEl.scrollTop = histEl.scrollHeight;
|
| 645 |
+
|
| 646 |
+
// toolbar tooltips + modal labels
|
| 647 |
+
document.getElementById("bb-btn-new").title = t("newGame");
|
| 648 |
+
document.getElementById("bb-btn-setup").title = t("setup");
|
| 649 |
+
document.getElementById("bb-btn-help").title = t("help");
|
| 650 |
+
document.getElementById("bb-btn-pgn").title = t("pgn");
|
| 651 |
+
document.getElementById("bb-setup-title").textContent = t("setup");
|
| 652 |
+
document.getElementById("bb-fen-help").innerHTML = t("fenHelp");
|
| 653 |
+
document.getElementById("bb-setup-apply").textContent = t("apply");
|
| 654 |
+
document.getElementById("bb-setup-close").textContent = t("close");
|
| 655 |
+
document.getElementById("bb-help-title").textContent = t("help");
|
| 656 |
+
document.getElementById("bb-help-intro").innerHTML = t("helpIntro");
|
| 657 |
+
document.getElementById("bb-help-keys").innerHTML =
|
| 658 |
+
t("keys").map((k) => "<kbd>" + k[0] + "</kbd><span>" + k[1] + "</span>").join("");
|
| 659 |
+
document.getElementById("bb-help-extra").textContent = t("helpExtra");
|
| 660 |
+
document.getElementById("bb-help-credit").textContent = t("authors");
|
| 661 |
+
document.getElementById("bb-help-close").textContent = t("close");
|
| 662 |
+
document.getElementById("bb-end-close").textContent = t("close");
|
| 663 |
+
document.getElementById("bb-end-pgn").textContent = t("pgn");
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
function anyModalOpen() {
|
| 667 |
+
return document.querySelector(".bb-overlay.open") !== null;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
// ---- events ------------------------------------------------------------ #
|
| 671 |
+
document.getElementById("bb-langsel").addEventListener("change", (e) => { lang = e.target.value; render(); });
|
| 672 |
+
|
| 673 |
+
document.getElementById("bb-toggle").addEventListener("click", () => {
|
| 674 |
+
const cols = document.getElementById("bb-cols");
|
| 675 |
+
const hidden = cols.classList.toggle("side-hidden");
|
| 676 |
+
document.getElementById("bb-toggle").textContent = hidden ? "⇤" : "⇥";
|
| 677 |
+
sizeBoard(); // board width changed — rescale stones
|
| 678 |
+
});
|
| 679 |
+
|
| 680 |
+
document.getElementById("bb-wrap").addEventListener("click", async (e) => {
|
| 681 |
+
const el = e.target.closest("[data-act]"); if (!el) return;
|
| 682 |
+
const act = el.dataset.act;
|
| 683 |
+
if (act === "new") { await newGame(DEFAULT_FEN); }
|
| 684 |
+
else if (act === "pgn") { downloadPgn(); }
|
| 685 |
+
else if (act === "open") {
|
| 686 |
+
const m = document.getElementById(el.dataset.modal);
|
| 687 |
+
if (el.dataset.modal === "bb-setup") {
|
| 688 |
+
document.getElementById("bb-fen").value = nodeToFen(line[cursor]);
|
| 689 |
+
document.getElementById("bb-setup-err").textContent = "";
|
| 690 |
+
}
|
| 691 |
+
m.classList.add("open");
|
| 692 |
+
}
|
| 693 |
+
else if (act === "close") { document.getElementById(el.dataset.modal).classList.remove("open"); }
|
| 694 |
+
else if (act === "apply") {
|
| 695 |
+
const fen = document.getElementById("bb-fen").value.trim();
|
| 696 |
+
const err = await newGame(fen);
|
| 697 |
+
if (err) document.getElementById("bb-setup-err").textContent = t("fenHelp");
|
| 698 |
+
else document.getElementById("bb-setup").classList.remove("open");
|
| 699 |
+
}
|
| 700 |
+
else if (act === "pit") { await inputIndex(parseInt(el.dataset.i, 10)); }
|
| 701 |
+
else if (act === "hist") { go(parseInt(el.dataset.idx, 10) + 1); }
|
| 702 |
+
});
|
| 703 |
+
document.querySelectorAll(".bb-overlay").forEach((o) => o.addEventListener("click", (e) => {
|
| 704 |
+
if (e.target === o) o.classList.remove("open");
|
| 705 |
+
}));
|
| 706 |
+
|
| 707 |
+
// Kazakh keyboard number row maps to the same digit inputs (item 5.2).
|
| 708 |
+
const KZ_DIGIT = { "«": "1", "ә": "2", "і": "3", "ң": "4", "ғ": "5", "ұ": "9", "қ": "0" };
|
| 709 |
+
// WASD navigation, case-insensitive, plus the letters those keys type on a
|
| 710 |
+
// Kazakh/Russian layout: W→ц, A→ф, S→ы, D→в (items 5.3 / 5.4).
|
| 711 |
+
const NAV = { w: "up", a: "left", s: "down", d: "right",
|
| 712 |
+
"ц": "up", "ф": "left", "ы": "down", "в": "right" };
|
| 713 |
+
|
| 714 |
+
document.addEventListener("keydown", (e) => {
|
| 715 |
+
if (anyModalOpen()) { if (e.key === "Escape") document.querySelectorAll(".bb-overlay.open").forEach((o) => o.classList.remove("open")); return; }
|
| 716 |
+
const tag = (document.activeElement && document.activeElement.tagName) || "";
|
| 717 |
+
if (tag === "TEXTAREA" || tag === "INPUT" || tag === "SELECT") return;
|
| 718 |
+
|
| 719 |
+
let key = e.key;
|
| 720 |
+
if (KZ_DIGIT[key] !== undefined) key = KZ_DIGIT[key]; // normalise Kazakh row to digits
|
| 721 |
+
const nav = NAV[key.toLowerCase()]; // WASD / Kazakh nav (case-insensitive)
|
| 722 |
+
|
| 723 |
+
if ((key >= "0" && key <= "5") || key === "9") { inputIndex(parseInt(key, 10)); e.preventDefault(); }
|
| 724 |
+
else if (e.key === "ArrowRight" || nav === "right") { go(cursor + 1); e.preventDefault(); }
|
| 725 |
+
else if (e.key === "ArrowLeft" || nav === "left") { go(cursor - 1); e.preventDefault(); }
|
| 726 |
+
else if (e.key === "ArrowUp" || nav === "up") { go(0); e.preventDefault(); }
|
| 727 |
+
else if (e.key === "ArrowDown" || nav === "down") { go(line.length - 1); e.preventDefault(); }
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
const boot = setInterval(() => {
|
| 731 |
+
if (document.querySelector("#oracle_in textarea") && document.querySelector("#oracle_btn")) {
|
| 732 |
+
clearInterval(boot); newGame(DEFAULT_FEN);
|
| 733 |
+
}
|
| 734 |
+
}, 100);
|
| 735 |
+
}
|
| 736 |
+
""".replace("__I18N_JSON__", json.dumps(TRANSLATIONS, ensure_ascii=False))
|
| 737 |
+
|
| 738 |
+
ensure_query_binary()
|
| 739 |
+
ensure_tablebase()
|
| 740 |
+
|
| 741 |
+
with gr.Blocks(title="Bestemshe — Perfect-Play Explorer", fill_width=True) as demo:
|
| 742 |
+
gr.HTML(INDEX_MARKUP)
|
| 743 |
+
inp = gr.Textbox(elem_id="oracle_in")
|
| 744 |
+
out = gr.Textbox(elem_id="oracle_out")
|
| 745 |
+
btn = gr.Button("bridge", elem_id="oracle_btn")
|
| 746 |
+
btn.click(oracle_bridge, inp, out)
|
| 747 |
+
demo.load(None, None, None, js=INDEX_JS)
|
| 748 |
+
|
| 749 |
+
if __name__ == "__main__":
|
| 750 |
+
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
|
commands.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ==============================================================================
|
| 2 |
+
# BESTEMSHE DISTRIBUTED PIPELINE COMMANDS (ZSTD NATIVE)
|
| 3 |
+
# Execute these commands sequentially on a cluster node for a specific pair.
|
| 4 |
+
# ==============================================================================
|
| 5 |
+
|
| 6 |
+
# Ensure directories exist
|
| 7 |
+
mkdir -p layers/compressed
|
| 8 |
+
|
| 9 |
+
# ------------------------------------------------------------------------------
|
| 10 |
+
# STEP 1: SOLVE THE SYMMETRIC PAIR
|
| 11 |
+
# ------------------------------------------------------------------------------
|
| 12 |
+
# Format: ./bestemshe --solve-pair <M> <K1> <K2>
|
| 13 |
+
# Example for M=10, K1=2, K2=8:
|
| 14 |
+
./bestemshe --solve-pair 10 2 8
|
| 15 |
+
|
| 16 |
+
# This automatically loads the strictly necessary reachable layers (e.g., M=12, M=14)
|
| 17 |
+
# and writes two files to disk:
|
| 18 |
+
# layers/layer_2_8_win.raw
|
| 19 |
+
# layers/layer_2_8_draw.raw
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ------------------------------------------------------------------------------
|
| 23 |
+
# STEP 2: VERIFY THE PAIR (CRITICAL)
|
| 24 |
+
# ------------------------------------------------------------------------------
|
| 25 |
+
# Format: ./bestemshe --verify-pair <M> <K1> <K2>
|
| 26 |
+
# Example:
|
| 27 |
+
./bestemshe --verify-pair 10 2 8
|
| 28 |
+
|
| 29 |
+
# This does a local lock-free forward sweep to ensure the generated .raw files
|
| 30 |
+
# are 100% mathematically consistent. If this fails, the node corrupted the data.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ------------------------------------------------------------------------------
|
| 34 |
+
# STEP 3: COMPRESS THE PAIR (ZSTD LEVEL 19)
|
| 35 |
+
# ------------------------------------------------------------------------------
|
| 36 |
+
# Format: ./bestemshe --compress <input.raw> <output.bin>
|
| 37 |
+
# Note: Block size and Algorithm are hardcoded now, no extra args needed.
|
| 38 |
+
|
| 39 |
+
./bestemshe --compress layers/layer_2_8_win.raw layers/compressed/layer_2_8_win.bin
|
| 40 |
+
./bestemshe --compress layers/layer_2_8_draw.raw layers/compressed/layer_2_8_draw.bin
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ------------------------------------------------------------------------------
|
| 44 |
+
# STEP 4: CLEANUP RAW FILES TO SAVE SSD SPACE
|
| 45 |
+
# ------------------------------------------------------------------------------
|
| 46 |
+
# Once compression is complete and verified, delete the raw files on the node.
|
| 47 |
+
rm layers/layer_2_8_win.raw
|
| 48 |
+
rm layers/layer_2_8_draw.raw
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ------------------------------------------------------------------------------
|
| 52 |
+
# ENDGAME: THE GOD QUERY
|
| 53 |
+
# ------------------------------------------------------------------------------
|
| 54 |
+
# When you finally solve M=0 (K1=0, K2=0), run:
|
| 55 |
+
./bestemshe --root
|
| 56 |
+
|
| 57 |
+
# This will load layer_0_0 and output the absolute game-theoretic truth of Bestemshe.
|
i18n.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single source of truth for every user-facing string, flag and language name.
|
| 2 |
+
|
| 3 |
+
`LANGUAGES` drives the language dropdown (code + flag + native name).
|
| 4 |
+
`TRANSLATIONS` holds every phrase, keyed by the same codes. app.py injects both
|
| 5 |
+
into the front-end as JSON, so there is exactly one place to edit copy.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# code -> flag emoji + native language name (order = dropdown order)
|
| 9 |
+
LANGUAGES = [
|
| 10 |
+
{"code": "EN", "flag": "🇬🇧", "name": "English"},
|
| 11 |
+
{"code": "KZ", "flag": "🇰🇿", "name": "Қазақша"},
|
| 12 |
+
{"code": "RU", "flag": "🇷🇺", "name": "Русский"},
|
| 13 |
+
{"code": "KG", "flag": "🇰🇬", "name": "Кыргызча"},
|
| 14 |
+
{"code": "TR", "flag": "🇹🇷", "name": "Türkçe"},
|
| 15 |
+
{"code": "CS", "flag": "🇨🇿", "name": "Čeština"},
|
| 16 |
+
{"code": "ES", "flag": "🇨🇴", "name": "Español"},
|
| 17 |
+
{"code": "UZ", "flag": "🇺🇿", "name": "O‘zbekcha"},
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
# The two roles. The first player always moves first ("the one who starts"),
|
| 21 |
+
# the second follows. Their display names are translated per language via the
|
| 22 |
+
# "nameFirst" / "nameSecond" keys below — no White/Black wording anywhere.
|
| 23 |
+
|
| 24 |
+
TRANSLATIONS = {
|
| 25 |
+
"EN": {
|
| 26 |
+
"nameFirst": "Beginner",
|
| 27 |
+
"nameSecond": "Follower",
|
| 28 |
+
"wins": "wins!",
|
| 29 |
+
"drawLoop": "Infinite loop — draw",
|
| 30 |
+
"loopHint": "The exact same board position repeated, so the game is a draw.",
|
| 31 |
+
"history": "Moves",
|
| 32 |
+
"newGame": "New Game",
|
| 33 |
+
"setup": "Set Position",
|
| 34 |
+
"help": "How to Play",
|
| 35 |
+
"pgn": "Download PGN",
|
| 36 |
+
"apply": "Start",
|
| 37 |
+
"close": "Close",
|
| 38 |
+
"fenHelp": "Set up any custom board using FEN format. Read from left to right:<br><br>"
|
| 39 |
+
"<b>1. Board:</b> Beginner's 5 pits, a slash (<b>/</b>), then Follower's 5 pits.<br>"
|
| 40 |
+
"<b>2. Kazans:</b> Beginner's kazan, then Follower's kazan.<br>"
|
| 41 |
+
"<b>3. Turn:</b> <b>w</b> for Beginner, <b>b</b> for Follower.<br>"
|
| 42 |
+
"<b>4. Move Number:</b> Current move count.<br><br>"
|
| 43 |
+
"<i>Note: All kumalaks must add up exactly to 50.</i><br><br>"
|
| 44 |
+
"<b>Example (Starting Position):</b><br>"
|
| 45 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 46 |
+
"helpIntro": "Bestemshe has been completely solved by computers. The glowing ring around each pit reveals "
|
| 47 |
+
"the guaranteed outcome of that move, assuming perfect play from both sides:<br><br>"
|
| 48 |
+
"🟢 <b>Green:</b> The corresponding player will win.<br>"
|
| 49 |
+
"🟡 <b>Yellow:</b> The game will be a draw via infinite loop.<br>"
|
| 50 |
+
"🔴 <b>Red:</b> The corresponding player will lose.<br><br>"
|
| 51 |
+
"Click on any of your pits to make a move.",
|
| 52 |
+
"keys": [
|
| 53 |
+
["1–5", "Play pits 1 through 5"],
|
| 54 |
+
["0", "Play a random optimal move"],
|
| 55 |
+
["9", "Play a totally random move"],
|
| 56 |
+
["← / A", "Go back one move"],
|
| 57 |
+
["→ / D", "Go forward one move"],
|
| 58 |
+
["↑ / W", "Return to the starting position"],
|
| 59 |
+
["↓ / S", "Jump to the latest move"],
|
| 60 |
+
],
|
| 61 |
+
"helpExtra": "Click any move in the history panel to jump back to that exact position. "
|
| 62 |
+
"If you play a different move from there, a new timeline is created and the old moves are overwritten. "
|
| 63 |
+
"The game ends in a draw if the exact same board position repeats.",
|
| 64 |
+
"authors": "Bestemshe Table Base, its Strong Solution proof, and this God's Algorithm were "
|
| 65 |
+
"created by Ansar Zeinulla & Murat Manassov.",
|
| 66 |
+
},
|
| 67 |
+
"KZ": {
|
| 68 |
+
"nameFirst": "Бастаушы",
|
| 69 |
+
"nameSecond": "Қостаушы",
|
| 70 |
+
"wins": "жеңеді!",
|
| 71 |
+
"drawLoop": "Шексіз цикл — тең ойын",
|
| 72 |
+
"loopHint": "Тақтадағы позиция қайталанды, сондықтан ойын тең аяқталды.",
|
| 73 |
+
"history": "Жүрістер",
|
| 74 |
+
"newGame": "Жаңа ойын",
|
| 75 |
+
"setup": "Позиция қою",
|
| 76 |
+
"help": "Қалай ойнайды?",
|
| 77 |
+
"pgn": "PGN жүктеу",
|
| 78 |
+
"apply": "Бастау",
|
| 79 |
+
"close": "Жабу",
|
| 80 |
+
"fenHelp": "FEN форматын пайдаланып кез келген позицияны қойыңыз. Солдан оңға қарай оқылады:<br><br>"
|
| 81 |
+
"<b>1. Тақта:</b> Бастаушының 5 ұясы, қиғаш сызық (<b>/</b>), сосын Қостаушының 5 ұясы.<br>"
|
| 82 |
+
"<b>2. Қазандар:</b> Бастаушының қазаны, сосын Қостаушының қазаны.<br>"
|
| 83 |
+
"<b>3. Кезек:</b> <b>w</b> — Бастаушы, <b>b</b> — Қостаушы.<br>"
|
| 84 |
+
"<b>4. Жүріс нөмірі:</b> Ағымдағы жүріс саны.<br><br>"
|
| 85 |
+
"<i>Ескертпе: Құмалақтардың жалпы саны әрқашан 50 болуы тиіс.</i><br><br>"
|
| 86 |
+
"<b>Мысал (Бастапқы позиция):</b><br>"
|
| 87 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 88 |
+
"helpIntro": "Бестемше ойыны компьютермен толық шешілген. Әр ұяның айналасындағы жарқыраған сақина екі жақтың мінсіз "
|
| 89 |
+
"ойыны кезіндегі сол жүрістің кепілдендірілген нәтижесін көрсетеді:<br><br>"
|
| 90 |
+
"🟢 <b>Жасыл:</b> Жүріс жасайтын ойыншы жеңеді.<br>"
|
| 91 |
+
"🟡 <b>Сары:</b> Ойын шексіз циклге байланысты тең аяқталады.<br>"
|
| 92 |
+
"🔴 <b>Қызыл:</b> Жүріс жасайтын ойыншы жеңіледі.<br><br>"
|
| 93 |
+
"Жүріс жасау үшін кез келген ұяңызды басыңыз.",
|
| 94 |
+
"keys": [
|
| 95 |
+
["1–5", "1-ден 5-ке дейінгі ұялардан ойнау"],
|
| 96 |
+
["0", "Кездейсоқ оңтайлы жүріс жасау"],
|
| 97 |
+
["9", "Толықтай кездейсоқ жүріс жасау"],
|
| 98 |
+
["← / A", "Бір жүріс артқа"],
|
| 99 |
+
["→ / D", "Бір жүріс алға"],
|
| 100 |
+
["↑ / W", "Бастапқы позицияға қайту"],
|
| 101 |
+
["↓ / S", "Соңғы жүріске өту"],
|
| 102 |
+
],
|
| 103 |
+
"helpExtra": "Тарих тақтасындағы кез келген жүрісті басып, дәл сол позицияға орала аласыз. "
|
| 104 |
+
"Егер ол жерден басқа жүріс жасасаңыз, жаңа тармақ құрылып, ескі жүрістер қайта жазылады. "
|
| 105 |
+
"Тақтадағы позиция қайталанса, ойын шексіз циклге түсіп, тең аяқталады.",
|
| 106 |
+
"authors": "Бестемше кестелік базасын, оның Толық Шешім дәлелін және осы Құдай алгоритмін "
|
| 107 |
+
"Аңсар Зейнулла мен Мұрат Манасов жасады.",
|
| 108 |
+
},
|
| 109 |
+
"RU": {
|
| 110 |
+
"nameFirst": "Начинающий",
|
| 111 |
+
"nameSecond": "Продолжающий",
|
| 112 |
+
"wins": "выигрывает!",
|
| 113 |
+
"drawLoop": "Бесконечный цикл — ничья",
|
| 114 |
+
"loopHint": "Позиция на доске повторилась, поэтому игра завершилась вничью.",
|
| 115 |
+
"history": "Ходы",
|
| 116 |
+
"newGame": "Новая игра",
|
| 117 |
+
"setup": "Задать позицию",
|
| 118 |
+
"help": "Как играть?",
|
| 119 |
+
"pgn": "Скачать PGN",
|
| 120 |
+
"apply": "Начать",
|
| 121 |
+
"close": "Закрыть",
|
| 122 |
+
"fenHelp": "Настройте любую позицию, используя формат FEN. Читается слева направо:<br><br>"
|
| 123 |
+
"<b>1. Доска:</b> 5 лунок Начинающего, затем слэш (<b>/</b>), затем 5 лунок Продолжающего.<br>"
|
| 124 |
+
"<b>2. Казаны:</b> казан Начинающего, затем казан Продолжающего.<br>"
|
| 125 |
+
"<b>3. Очередь хода:</b> <b>w</b> — Начинающий, <b>b</b> — Продолжающий.<br>"
|
| 126 |
+
"<b>4. Номер хода:</b> Текущий номер хода.<br><br>"
|
| 127 |
+
"<i>Примечание: Общее количество кумалаков всегда должно быть равно 50.</i><br><br>"
|
| 128 |
+
"<b>Пример (Начальная позиция):</b><br>"
|
| 129 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 130 |
+
"helpIntro": "Игра Бестемше полностью решена компьютерами. Светящееся кольцо вокруг каждой лунки "
|
| 131 |
+
"показывает гарантированный исход этого хода при идеальной игре обеих сторон:<br><br>"
|
| 132 |
+
"🟢 <b>Зеленый:</b> Ходящий игрок выиграет.<br>"
|
| 133 |
+
"🟡 <b>Желтый:</b> Будет ничья благодаря бесконечному циклу.<br>"
|
| 134 |
+
"🔴 <b>Красный:</b> Ходящий игрок проиграет.<br><br>"
|
| 135 |
+
"Нажмите на любую из ваших лунок, чтобы сделать ход.",
|
| 136 |
+
"keys": [
|
| 137 |
+
["1–5", "Сыграть лунки с 1 по 5"],
|
| 138 |
+
["0", "Сделать случайный оптимальный ход"],
|
| 139 |
+
["9", "Сделать абсолютно случайный ход"],
|
| 140 |
+
["← / A", "На один ход назад"],
|
| 141 |
+
["→ / D", "На один ход вперед"],
|
| 142 |
+
["↑ / W", "Вернуться к начальной позиции"],
|
| 143 |
+
["↓ / S", "Перейти к последнему ходу"],
|
| 144 |
+
],
|
| 145 |
+
"helpExtra": "Нажмите на любой ход в панели истории, чтобы вернуться точно к этой позиции. "
|
| 146 |
+
"Если вы сделаете оттуда другой ход, создастся новая ветка, а старые ходы будут удалены. "
|
| 147 |
+
"Если позиция на доске полностью повторяется, игра заканчивается вничью из-за бесконечного цикла.",
|
| 148 |
+
"authors": "Табличную базу Бестемше, доказательство её Полного Решения и этот Алгоритм Бога "
|
| 149 |
+
"создали Ансар Зейнулла и Мурат Манасов.",
|
| 150 |
+
},
|
| 151 |
+
"KG": {
|
| 152 |
+
"nameFirst": "Баштоочу",
|
| 153 |
+
"nameSecond": "Коштоочу",
|
| 154 |
+
"wins": "жеңет!",
|
| 155 |
+
"drawLoop": "Түгөнбөс цикл — тең чыгуу",
|
| 156 |
+
"loopHint": "Тактадагы позиция кайталанды, ошондуктан оюн тең чыгуу менен аяктады.",
|
| 157 |
+
"history": "Жүрүштөр",
|
| 158 |
+
"newGame": "Жаңы оюн",
|
| 159 |
+
"setup": "Позиция коюу",
|
| 160 |
+
"help": "Кантип ойнойт?",
|
| 161 |
+
"pgn": "PGN жүктөө",
|
| 162 |
+
"apply": "Баштоо",
|
| 163 |
+
"close": "Жабуу",
|
| 164 |
+
"fenHelp": "FEN форматын колдонуп каалаган позицияны коюңуз. Солдон оңго карай окулат:<br><br>"
|
| 165 |
+
"<b>1. Такта:</b> Баштоочунун 5 уясы, кыйгач сызык (<b>/</b>), андан кийин Коштоочунун 5 уясы.<br>"
|
| 166 |
+
"<b>2. Казандар:</b> Баштоочунун казаны, андан кийин Коштоочунун казаны.<br>"
|
| 167 |
+
"<b>3. Кезек:</b> <b>w</b> — Баштоочу, <b>b</b> — Коштоочу.<br>"
|
| 168 |
+
"<b>4. Жүрүш номери:</b> Учурдагы жүрүш саны.<br><br>"
|
| 169 |
+
"<i>Эскертүү: Кумалактардын жалпы саны ар дайым 50 болушу керек.</i><br><br>"
|
| 170 |
+
"<b>Мисал (Баштапкы позиция):</b><br>"
|
| 171 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 172 |
+
"helpIntro": "Бестемше оюну компьютерлер тарабынан толук чечилген. Ар бир уянын айланасындагы жарык шакек "
|
| 173 |
+
"эки тараптын мыкты оюнундагы ошол жүрүштүн кепилденген натыйжасын көрсөтөт:<br><br>"
|
| 174 |
+
"🟢 <b>Жашыл:</b> Жүрүш жасоочу оюнчу утат.<br>"
|
| 175 |
+
"🟡 <b>Сары:</b> Оюн түгөнбөс циклге байланыштуу тең чыгуу менен аяктайт.<br>"
|
| 176 |
+
"🔴 <b>Кызыл:</b> Жүрүш жасоочу утулат.<br><br>"
|
| 177 |
+
"Жүрүш жасоо үчүн каалаган уяңызды басыңыз.",
|
| 178 |
+
"keys": [
|
| 179 |
+
["1–5", "1ден 5ке чейинки уяларды ойноо"],
|
| 180 |
+
["0", "Кокустан оптималдуу жүрүш жасоо"],
|
| 181 |
+
["9", "Толугу менен кокус жүрүш жасоо"],
|
| 182 |
+
["← / A", "Бир жүрүш артка"],
|
| 183 |
+
["→ / D", "Бир жүрүш алдыга"],
|
| 184 |
+
["↑ / W", "Баштапкы позицияга кайтуу"],
|
| 185 |
+
["↓ / S", "Акыркы жүрүшкө өтүү"],
|
| 186 |
+
],
|
| 187 |
+
"helpExtra": "Тарых тактасындагы каалаган жүрүштү басып, так ошол позицияга кайта аласыз. "
|
| 188 |
+
"Эгер ал жерден башка жүрүш жасасаңыз, жаңы бутак түзүлүп, эски жүрүштөр кайра жазылат. "
|
| 189 |
+
"Тактадагы позиция кайталанса, оюн түгөнбөс циклге түшүп, тең чыгуу менен аяктайт.",
|
| 190 |
+
"authors": "Бестемше таблицалык базасын, анын Толук Чечим далилин жана бул Кудай алгоритмин "
|
| 191 |
+
"Ансар Зейнулла жана Мурат Манасов түзгөн.",
|
| 192 |
+
},
|
| 193 |
+
"TR": {
|
| 194 |
+
"nameFirst": "Başlayan",
|
| 195 |
+
"nameSecond": "İzleyen",
|
| 196 |
+
"wins": "kazandı!",
|
| 197 |
+
"drawLoop": "Sonsuz döngü — Beraberlik",
|
| 198 |
+
"loopHint": "Tahtadaki konum tamamen tekrarlandığı için oyun berabere bitti.",
|
| 199 |
+
"history": "Hamleler",
|
| 200 |
+
"newGame": "Yeni Oyun",
|
| 201 |
+
"setup": "Konum Ayarla",
|
| 202 |
+
"help": "Nasıl Oynanır?",
|
| 203 |
+
"pgn": "PGN İndir",
|
| 204 |
+
"apply": "Başlat",
|
| 205 |
+
"close": "Kapat",
|
| 206 |
+
"fenHelp": "FEN formatını kullanarak özel bir konum ayarlayın. Soldan sağa doğru okunur:<br><br>"
|
| 207 |
+
"<b>1. Tahta:</b> Başlayan'ın 5 kuyusu, eğik çizgi (<b>/</b>), sonra İzleyen'in 5 kuyusu.<br>"
|
| 208 |
+
"<b>2. Kazanlar:</b> Başlayan'ın kazanı, sonra İzleyen'in kazanı.<br>"
|
| 209 |
+
"<b>3. Sıra:</b> <b>w</b> — Başlayan, <b>b</b> — İzleyen.<br>"
|
| 210 |
+
"<b>4. Hamle Numarası:</b> Mevcut hamle sayısı.<br><br>"
|
| 211 |
+
"<i>Not: Toplam kumalak sayısı her zaman tam olarak 50 olmalıdır.</i><br><br>"
|
| 212 |
+
"<b>Örnek (Başlangıç Konumu):</b><br>"
|
| 213 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 214 |
+
"helpIntro": "Bestemshe oyunu bilgisayarlar tarafından tamamen çözülmüştür. Her kuyunun etrafındaki parlayan halka, "
|
| 215 |
+
"her iki tarafın da kusursuz oynadığı varsayılarak o hamlenin kesin sonucunu gösterir:<br><br>"
|
| 216 |
+
"🟢 <b>Yeşil:</b> Hamleyi yapan oyuncu kazanır.<br>"
|
| 217 |
+
"🟡 <b>Sarı:</b> Oyun sonsuz döngü nedeniyle berabere biter.<br>"
|
| 218 |
+
"🔴 <b>Kırmızı:</b> Hamleyi yapan oyuncu kaybeder.<br><br>"
|
| 219 |
+
"Hamle yapmak için kendi kuyularınızdan birine tıklayın.",
|
| 220 |
+
"keys": [
|
| 221 |
+
["1–5", "1'den 5'e kadar olan kuyulardan birini oynayın"],
|
| 222 |
+
["0", "Rastgele en iyi (optimum) hamleyi yapın"],
|
| 223 |
+
["9", "Tamamen rastgele bir hamle yapın"],
|
| 224 |
+
["← / A", "Bir hamle geri git"],
|
| 225 |
+
["→ / D", "Bir hamle ileri git"],
|
| 226 |
+
["↑ / W", "Başlangıç konumuna dön"],
|
| 227 |
+
["↓ / S", "Son hamleye atla"],
|
| 228 |
+
],
|
| 229 |
+
"helpExtra": "Geçmiş panelindeki herhangi bir hamleye tıklayarak tam o konuma geri dönebilirsiniz. "
|
| 230 |
+
"Oradan farklı bir hamle yaparsanız yeni bir varyant (dal) oluşur ve sonraki eski hamleler silinir. "
|
| 231 |
+
"Tahtadaki bir konum tam olarak tekrarlanırsa, oyun sonsuz döngü nedeniyle berabere biter.",
|
| 232 |
+
"authors": "Bestemshe Tablo Tabanı, Güçlü Çözüm kanıtı ve bu Tanrı Algoritması "
|
| 233 |
+
"Ansar Zeinulla ve Murat Manassov tarafından oluşturulmuştur.",
|
| 234 |
+
},
|
| 235 |
+
"CS": {
|
| 236 |
+
"nameFirst": "Začínající",
|
| 237 |
+
"nameSecond": "Následující",
|
| 238 |
+
"wins": "vyhrává!",
|
| 239 |
+
"drawLoop": "Nekonečná smyčka — remíza",
|
| 240 |
+
"loopHint": "Pozice na desce se zopakovala, takže hra končí remízou.",
|
| 241 |
+
"history": "Tahy",
|
| 242 |
+
"newGame": "Nová hra",
|
| 243 |
+
"setup": "Nastavit pozici",
|
| 244 |
+
"help": "Jak hrát?",
|
| 245 |
+
"pgn": "Stáhnout PGN",
|
| 246 |
+
"apply": "Začít",
|
| 247 |
+
"close": "Zavřít",
|
| 248 |
+
"fenHelp": "Nastavte si libovolnou pozici pomocí formátu FEN. Čte se zleva doprava:<br><br>"
|
| 249 |
+
"<b>1. Deska:</b> 5 jamek Začínajícího, lomítko (<b>/</b>), poté 5 jamek Následujícího.<br>"
|
| 250 |
+
"<b>2. Kazany:</b> kazan Začínajícího, poté kazan Následujícího.<br>"
|
| 251 |
+
"<b>3. Tah:</b> <b>w</b> — Začínající, <b>b</b> — Následující.<br>"
|
| 252 |
+
"<b>4. Číslo tahu:</b> Aktuální počet tahů.<br><br>"
|
| 253 |
+
"<i>Poznámka: Celkový počet kumalaků musí být přesně 50.</i><br><br>"
|
| 254 |
+
"<b>Příklad (Výchozí pozice):</b><br>"
|
| 255 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 256 |
+
"helpIntro": "Hra Bestemshe byla kompletně vyřešena počítači. Zářící kruh kolem každé jamky ukazuje zaručený "
|
| 257 |
+
"výsledek daného tahu za předpokladu dokonalé hry obou stran:<br><br>"
|
| 258 |
+
"🟢 <b>Zelená:</b> Hráč na tahu vyhraje.<br>"
|
| 259 |
+
"🟡 <b>Žlutá:</b> Hra skončí remízou kvůli nekonečné smyčce.<br>"
|
| 260 |
+
"🔴 <b>Červená:</b> Hráč na tahu prohraje.<br><br>"
|
| 261 |
+
"Klikněte na jakoukoli ze svých jamek a zahrajte tah.",
|
| 262 |
+
"keys": [
|
| 263 |
+
["1–5", "Zahrát jamky 1 až 5"],
|
| 264 |
+
["0", "Zahrát náhodný optimální tah"],
|
| 265 |
+
["9", "Zahrát zcela náhodný tah"],
|
| 266 |
+
["← / A", "O jeden tah zpět"],
|
| 267 |
+
["→ / D", "O jeden tah vpřed"],
|
| 268 |
+
["↑ / W", "Návrat do výchozí pozice"],
|
| 269 |
+
["↓ / S", "Skočit na poslední tah"],
|
| 270 |
+
],
|
| 271 |
+
"helpExtra": "Kliknutím na jakýkoli tah v panelu historie přeskočíte přesně do dané pozice. "
|
| 272 |
+
"Pokud odtud zahrajete jiný tah, vytvoří se nová časová osa a staré tahy budou smazány. "
|
| 273 |
+
"Pokud se pozice na desce přesně zopakuje, hra končí remízou kvůli nekonečné smyčce.",
|
| 274 |
+
"authors": "Databázi koncovek (Table Base) pro hru Bestemshe, její matematický důkaz a tento Boží algoritmus "
|
| 275 |
+
"vytvořili Ansar Zeinulla a Murat Manassov.",
|
| 276 |
+
},
|
| 277 |
+
"ES": {
|
| 278 |
+
"nameFirst": "Iniciador",
|
| 279 |
+
"nameSecond": "Seguidor",
|
| 280 |
+
"wins": "¡gana!",
|
| 281 |
+
"drawLoop": "Bucle infinito — Empate",
|
| 282 |
+
"loopHint": "La posición en el tablero se repitió por completo, así que el juego termina en empate.",
|
| 283 |
+
"history": "Jugadas",
|
| 284 |
+
"newGame": "Juego Nuevo",
|
| 285 |
+
"setup": "Configurar Posición",
|
| 286 |
+
"help": "¿Cómo se juega?",
|
| 287 |
+
"pgn": "Descargar PGN",
|
| 288 |
+
"apply": "Empezar",
|
| 289 |
+
"close": "Cerrar",
|
| 290 |
+
"fenHelp": "Configura cualquier posición personalizada usando el formato FEN. Se lee de izquierda a derecha:<br><br>"
|
| 291 |
+
"<b>1. Tablero:</b> 5 hoyos del Iniciador, una barra (<b>/</b>), luego 5 hoyos del Seguidor.<br>"
|
| 292 |
+
"<b>2. Kazanes:</b> kazán del Iniciador, luego kazán del Seguidor.<br>"
|
| 293 |
+
"<b>3. Turno:</b> <b>w</b> — Iniciador, <b>b</b> — Seguidor.<br>"
|
| 294 |
+
"<b>4. Número de jugada:</b> Conteo actual de jugadas.<br><br>"
|
| 295 |
+
"<i>Nota: El total de kumalaks (fichas) siempre debe sumar exactamente 50.</i><br><br>"
|
| 296 |
+
"<b>Ejemplo (Posición inicial):</b><br>"
|
| 297 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 298 |
+
"helpIntro": "El Bestemshe ha sido completamente resuelto por computadoras. El anillo brillante alrededor de cada hoyo "
|
| 299 |
+
"muestra el resultado garantizado de esa jugada, asumiendo que ambos juegan a la perfección:<br><br>"
|
| 300 |
+
"🟢 <b>Verde:</b> El jugador que mueve ganará.<br>"
|
| 301 |
+
"🟡 <b>Amarillo:</b> El juego será un empate por bucle infinito.<br>"
|
| 302 |
+
"🔴 <b>Rojo:</b> El jugador que mueve perderá.<br><br>"
|
| 303 |
+
"Haz clic en cualquiera de tus hoyos para hacer una jugada.",
|
| 304 |
+
"keys": [
|
| 305 |
+
["1–5", "Jugar los hoyos del 1 al 5"],
|
| 306 |
+
["0", "Hacer una jugada óptima al azar"],
|
| 307 |
+
["9", "Hacer una jugada totalmente al azar"],
|
| 308 |
+
["← / A", "Retroceder una jugada"],
|
| 309 |
+
["→ / D", "Avanzar una jugada"],
|
| 310 |
+
["↑ / W", "Volver a la posición inicial"],
|
| 311 |
+
["↓ / S", "Saltar a la última jugada"],
|
| 312 |
+
],
|
| 313 |
+
"helpExtra": "Haz clic en cualquier jugada en el panel de historial para volver a esa posición exacta. "
|
| 314 |
+
"Si haces una jugada diferente desde ahí, se crea una nueva variante y las jugadas anteriores se borran. "
|
| 315 |
+
"Si la misma posición en el tablero se repite, el juego termina en empate por bucle infinito.",
|
| 316 |
+
"authors": "La base de datos de tablas (Table Base) de Bestemshe, su demostración de Solución Fuerte y este Algoritmo "
|
| 317 |
+
"de Dios fueron creados por Ansar Zeinulla y Murat Manassov.",
|
| 318 |
+
},
|
| 319 |
+
"UZ": {
|
| 320 |
+
"nameFirst": "Boshlovchi",
|
| 321 |
+
"nameSecond": "Ergashuvchi",
|
| 322 |
+
"wins": "yutadi!",
|
| 323 |
+
"drawLoop": "Cheksiz sikl — durang",
|
| 324 |
+
"loopHint": "Taxtadagi holat to'liq takrorlandi, shuning uchun o'yin durang bilan tugadi.",
|
| 325 |
+
"history": "Yurishlar",
|
| 326 |
+
"newGame": "Yangi o‘yin",
|
| 327 |
+
"setup": "Holatni o‘rnatish",
|
| 328 |
+
"help": "Qanday o‘ynaladi?",
|
| 329 |
+
"pgn": "PGN yuklab olish",
|
| 330 |
+
"apply": "Boshlash",
|
| 331 |
+
"close": "Yopish",
|
| 332 |
+
"fenHelp": "FEN formati yordamida istalgan holatni o'rnating. Chapdan o'ngga o'qiladi:<br><br>"
|
| 333 |
+
"<b>1. Taxta:</b> Boshlovchining 5 ta uyasi, qiya chiziq (<b>/</b>), so'ngra Ergashuvchining 5 ta uyasi.<br>"
|
| 334 |
+
"<b>2. Qozonlar:</b> Boshlovchining qozoni, keyin Ergashuvchining qozoni.<br>"
|
| 335 |
+
"<b>3. Navbat:</b> <b>w</b> — Boshlovchi, <b>b</b> — Ergashuvchi.<br>"
|
| 336 |
+
"<b>4. Yurish raqami:</b> Joriy yurish soni.<br><br>"
|
| 337 |
+
"<i>Eslatma: Qumaloqlar (toshlar) ning umumiy soni doim 50 ta bo'lishi kerak.</i><br><br>"
|
| 338 |
+
"<b>Misol (Boshlang'ich holat):</b><br>"
|
| 339 |
+
"<code>5,5,5,5,5/5,5,5,5,5 0,0 w 1</code>",
|
| 340 |
+
"helpIntro": "Bestemshe o'yini kompyuterlar tomonidan to'liq yechilgan. Har bir uya atrofidagi porlayotgan halqa "
|
| 341 |
+
"ikkala tomonning mukammal o'yinini hisobga olgan holda, shu yurishning kafolatlangan natijasini ko'rsatadi:<br><br>"
|
| 342 |
+
"🟢 <b>Yashil:</b> Yurish qilayotgan o'yinchi yutadi.<br>"
|
| 343 |
+
"🟡 <b>Sariq:</b> O'yin cheksiz sikl tufayli durang bilan tugaydi.<br>"
|
| 344 |
+
"🔴 <b>Qizil:</b> Yurish qilayotgan o'yinchi yutqazadi.<br><br>"
|
| 345 |
+
"Yurish qilish uchun o'z uyalaringizdan birini bosing.",
|
| 346 |
+
"keys": [
|
| 347 |
+
["1–5", "1 dan 5 gacha bo'lgan uyalarni o'ynash"],
|
| 348 |
+
["0", "Tasodifiy eng yaxshi yurishni qilish"],
|
| 349 |
+
["9", "To'liq tasodifiy yurish qilish"],
|
| 350 |
+
["← / A", "Bir yurish orqaga"],
|
| 351 |
+
["→ / D", "Bir yurish oldinga"],
|
| 352 |
+
["↑ / W", "Boshlang'ich holatga qaytish"],
|
| 353 |
+
["↓ / S", "Oxirgi yurishga o'tish"],
|
| 354 |
+
],
|
| 355 |
+
"helpExtra": "Tarix panelidagi istalgan yurishni bosib, aynan shu holatga qaytishingiz mumkin. "
|
| 356 |
+
"Agar u yerdan boshqa yurish qilsangiz, yangi tarmoq yaratiladi va eski yurishlar o'chiriladi. "
|
| 357 |
+
"Taxtadagi holat to'liq takrorlansa, o'yin cheksiz sikl sababli durang bilan tugaydi.",
|
| 358 |
+
"authors": "Bestemshe jadval bazasi, uning To'liq Yechim isboti va ushbu Xudo Algoritmi "
|
| 359 |
+
"Ansar Zeinulla va Murat Manassov tomonidan yaratilgan.",
|
| 360 |
+
},
|
| 361 |
+
}
|
main.cpp
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "StateIndex.h"
|
| 2 |
+
#include "Solver.h"
|
| 3 |
+
#include "Compressor.h"
|
| 4 |
+
#include "Inference.h"
|
| 5 |
+
#include <iostream>
|
| 6 |
+
#include <fstream>
|
| 7 |
+
#include <string>
|
| 8 |
+
#include <filesystem>
|
| 9 |
+
#include <chrono>
|
| 10 |
+
|
| 11 |
+
using namespace Bestemshe;
|
| 12 |
+
|
| 13 |
+
void PrintUsage() {
|
| 14 |
+
std::cout << "========================================================\n"
|
| 15 |
+
<< " BESTEMSHE HPC ENGINE (God Algorithm / ZSTD Edition) \n"
|
| 16 |
+
<< "========================================================\n"
|
| 17 |
+
<< "Usage:\n"
|
| 18 |
+
<< " ./bestemshe --solve-pair <M> <K1> <K2>\n"
|
| 19 |
+
<< " ./bestemshe --verify-pair <M> <K1> <K2>\n"
|
| 20 |
+
<< " ./bestemshe --compress <input.raw> <output.bin>\n"
|
| 21 |
+
<< " ./bestemshe --decompress <input.bin> <output.raw> <expected_bytes>\n"
|
| 22 |
+
<< " ./bestemshe --root\n"
|
| 23 |
+
<< "========================================================\n";
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
int main(int argc, char* argv[]) {
|
| 27 |
+
StateIndex::InitCombinatorics();
|
| 28 |
+
|
| 29 |
+
if (argc < 2) {
|
| 30 |
+
PrintUsage();
|
| 31 |
+
return 1;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
std::string mode = argv[1];
|
| 35 |
+
|
| 36 |
+
if (mode == "--solve-pair" && argc == 5) {
|
| 37 |
+
uint8_t M = std::stoi(argv[2]);
|
| 38 |
+
uint16_t K1 = std::stoi(argv[3]);
|
| 39 |
+
uint16_t K2 = std::stoi(argv[4]);
|
| 40 |
+
std::cout << "[RUNNING] Solving Pair (" << K1 << "," << K2 << ") in Layer M = " << static_cast<int>(M) << "...\n";
|
| 41 |
+
|
| 42 |
+
InferenceEngine db;
|
| 43 |
+
RetrogradeSolver solver(M, &db);
|
| 44 |
+
solver.solve_pair_lock_free(K1, K2);
|
| 45 |
+
}
|
| 46 |
+
else if (mode == "--verify-pair" && argc == 5) {
|
| 47 |
+
uint8_t M = std::stoi(argv[2]);
|
| 48 |
+
uint16_t K1 = std::stoi(argv[3]);
|
| 49 |
+
uint16_t K2 = std::stoi(argv[4]);
|
| 50 |
+
std::cout << "[RUNNING] Verifying Pair (" << K1 << "," << K2 << ") in Layer M = " << static_cast<int>(M) << "...\n";
|
| 51 |
+
|
| 52 |
+
InferenceEngine db;
|
| 53 |
+
RetrogradeSolver solver(M, &db);
|
| 54 |
+
solver.verify_pair_consistency(K1, K2);
|
| 55 |
+
}
|
| 56 |
+
else if (mode == "--compress" && argc == 4) {
|
| 57 |
+
std::string in_raw = argv[2];
|
| 58 |
+
std::string out_bin = argv[3];
|
| 59 |
+
std::cout << "[RUNNING] Compressing " << in_raw << " -> " << out_bin << " (ZSTD L19)...\n";
|
| 60 |
+
Compressor::CompressMicroLayer(in_raw, out_bin); // Defaults to 32MB blocks
|
| 61 |
+
}
|
| 62 |
+
else if (mode == "--decompress" && argc == 5) {
|
| 63 |
+
std::string in_bin = argv[2];
|
| 64 |
+
std::string out_raw = argv[3];
|
| 65 |
+
size_t expected_bytes = std::stoull(argv[4]);
|
| 66 |
+
std::cout << "[RUNNING] Decompressing " << in_bin << " -> " << out_raw << "...\n";
|
| 67 |
+
|
| 68 |
+
// 33554432 bits = 4194304 bytes per block
|
| 69 |
+
std::vector<uint8_t> raw = Compressor::DecompressMicroLayer(in_bin, 4194304, expected_bytes);
|
| 70 |
+
if (!raw.empty()) {
|
| 71 |
+
std::ofstream out(out_raw, std::ios::binary);
|
| 72 |
+
out.write(reinterpret_cast<const char*>(raw.data()), raw.size());
|
| 73 |
+
std::cout << "[SUCCESS] Decompressed.\n";
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
else if (mode == "--root" && argc == 2) {
|
| 77 |
+
std::cout << "========================================================\n"
|
| 78 |
+
<< " BESTEMSHE GAME-THEORETIC SOLUTION FINDER \n"
|
| 79 |
+
<< "========================================================\n";
|
| 80 |
+
|
| 81 |
+
// Define the exact starting board of Bestemshe
|
| 82 |
+
State root;
|
| 83 |
+
root.M = 0; root.K_self = 0; root.K_opp = 0;
|
| 84 |
+
for (int i = 0; i < 10; ++i) root.board[i] = 5;
|
| 85 |
+
|
| 86 |
+
uint64_t root_idx = StateIndex::IndexState(root);
|
| 87 |
+
|
| 88 |
+
InferenceEngine db;
|
| 89 |
+
auto t_start = std::chrono::high_resolution_clock::now();
|
| 90 |
+
GameValue result = db.query_state(0, 0, root_idx);
|
| 91 |
+
auto t_end = std::chrono::high_resolution_clock::now();
|
| 92 |
+
|
| 93 |
+
std::chrono::duration<double> d_query = t_end - t_start;
|
| 94 |
+
|
| 95 |
+
std::cout << "Result for First Player (Player 1): ";
|
| 96 |
+
if (result == GameValue::WIN) std::cout << "\033[1;32mWIN (P1 forces a win)\033[0m\n";
|
| 97 |
+
else if (result == GameValue::LOSS) std::cout << "\033[1;31mLOSS (P2 forces a win)\033[0m\n";
|
| 98 |
+
else if (result == GameValue::DRAW) std::cout << "\033[1;33mDRAW (Perfect play is a draw)\033[0m\n";
|
| 99 |
+
else std::cout << "\033[1;31mUNKNOWN (Error: Layer 0 not fully solved/loaded!)\033[0m\n";
|
| 100 |
+
|
| 101 |
+
std::cout << "--------------------------------------------------------\n";
|
| 102 |
+
std::cout << "Root Query Execution Time: " << d_query.count() << " seconds\n";
|
| 103 |
+
std::cout << "========================================================\n";
|
| 104 |
+
}else if (mode == "--analyze") {
|
| 105 |
+
State s;
|
| 106 |
+
// Accept either a custom board, or default to the starting position
|
| 107 |
+
if (argc == 14) {
|
| 108 |
+
s.K_self = static_cast<uint8_t>(std::stoi(argv[2]));
|
| 109 |
+
s.K_opp = static_cast<uint8_t>(std::stoi(argv[3]));
|
| 110 |
+
s.M = s.K_self + s.K_opp;
|
| 111 |
+
for (int i = 0; i < 10; ++i) s.board[i] = static_cast<uint8_t>(std::stoi(argv[4 + i]));
|
| 112 |
+
} else if (argc == 2) {
|
| 113 |
+
s.M = 0; s.K_self = 0; s.K_opp = 0;
|
| 114 |
+
for (int i = 0; i < 10; ++i) s.board[i] = 5;
|
| 115 |
+
} else {
|
| 116 |
+
std::cout << "Usage: ./bestemshe --analyze [OR] ./bestemshe --analyze K1 K2 p0..p9\n";
|
| 117 |
+
return 1;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
InferenceEngine db;
|
| 121 |
+
int ply = 0;
|
| 122 |
+
|
| 123 |
+
while (true) {
|
| 124 |
+
bool is_p1_turn = (ply % 2 == 0);
|
| 125 |
+
std::string current_player = is_p1_turn ? "Player 1 (White)" : "Player 2 (Black)";
|
| 126 |
+
|
| 127 |
+
// 1. Construct Absolute Board for Fixed Human Perspective
|
| 128 |
+
uint8_t abs_board[10];
|
| 129 |
+
int abs_k1, abs_k2;
|
| 130 |
+
if (is_p1_turn) {
|
| 131 |
+
for (int i = 0; i < 10; ++i) abs_board[i] = s.board[i];
|
| 132 |
+
abs_k1 = s.K_self;
|
| 133 |
+
abs_k2 = s.K_opp;
|
| 134 |
+
} else {
|
| 135 |
+
for (int i = 0; i < 5; ++i) {
|
| 136 |
+
abs_board[i] = s.board[i + 5]; // P1's pits are in 5-9 of canonical state
|
| 137 |
+
abs_board[i + 5] = s.board[i]; // P2's pits are in 0-4 of canonical state
|
| 138 |
+
}
|
| 139 |
+
abs_k1 = s.K_opp;
|
| 140 |
+
abs_k2 = s.K_self;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
std::cout << "\n========================================================\n"
|
| 144 |
+
<< " BESTEMSHE ORACLE: PLY " << ply << " | " << current_player << " to move\n"
|
| 145 |
+
<< "========================================================\n";
|
| 146 |
+
|
| 147 |
+
std::cout << " [P2 Kazan (Black): " << abs_k2 << "]\n\n";
|
| 148 |
+
std::cout << " (9) (8) (7) (6) (5) <- P2 Pits\n ";
|
| 149 |
+
for (int i = 9; i >= 5; --i) printf("[%2d] ", (int)abs_board[i]);
|
| 150 |
+
std::cout << "\n\n ";
|
| 151 |
+
for (int i = 0; i < 5; ++i) printf("[%2d] ", (int)abs_board[i]);
|
| 152 |
+
std::cout << "\n (0) (1) (2) (3) (4) <- P1 Pits\n\n";
|
| 153 |
+
std::cout << " [P1 Kazan (White): " << abs_k1 << "]\n";
|
| 154 |
+
std::cout << "--------------------------------------------------------\n";
|
| 155 |
+
std::cout << "EVALUATING MOVES (1-0 = P1 Wins, 0-1 = P2 Wins)\n";
|
| 156 |
+
|
| 157 |
+
int start_move = is_p1_turn ? 0 : 5;
|
| 158 |
+
int end_move = is_p1_turn ? 4 : 9;
|
| 159 |
+
bool has_moves = false;
|
| 160 |
+
|
| 161 |
+
for (int abs_move = start_move; abs_move <= end_move; ++abs_move) {
|
| 162 |
+
int canonical_move = is_p1_turn ? abs_move : (abs_move - 5);
|
| 163 |
+
|
| 164 |
+
if (s.board[canonical_move] == 0) {
|
| 165 |
+
std::cout << "Move " << abs_move << ": INVALID (Empty pit)\n";
|
| 166 |
+
continue;
|
| 167 |
+
}
|
| 168 |
+
has_moves = true;
|
| 169 |
+
|
| 170 |
+
State next_s;
|
| 171 |
+
bool empties;
|
| 172 |
+
ExecuteMoveAndFlip(s, canonical_move, next_s, empties);
|
| 173 |
+
|
| 174 |
+
std::string result_str;
|
| 175 |
+
|
| 176 |
+
if (empties || next_s.K_opp >= 26) {
|
| 177 |
+
if (is_p1_turn) result_str = "\033[1;32m1-0 (P1 forces WIN - Immediate)\033[0m";
|
| 178 |
+
else result_str = "\033[1;31m0-1 (P2 forces WIN - Immediate)\033[0m";
|
| 179 |
+
} else {
|
| 180 |
+
uint64_t next_idx = StateIndex::IndexState(next_s);
|
| 181 |
+
GameValue val = db.query_state(next_s.M, next_s.K_opp, next_idx);
|
| 182 |
+
|
| 183 |
+
if (val == GameValue::DRAW) {
|
| 184 |
+
result_str = "\033[1;33m0.5-0.5 (Forced DRAW)\033[0m";
|
| 185 |
+
} else if (val == GameValue::UNKNOWN) {
|
| 186 |
+
result_str = "\033[1;31mUNKNOWN (Error: Layer missing)\033[0m";
|
| 187 |
+
} else {
|
| 188 |
+
bool next_player_wins = (val == GameValue::WIN);
|
| 189 |
+
bool p1_wins = is_p1_turn ? !next_player_wins : next_player_wins;
|
| 190 |
+
|
| 191 |
+
if (p1_wins) result_str = "\033[1;32m1-0 (P1 forces WIN)\033[0m";
|
| 192 |
+
else result_str = "\033[1;31m0-1 (P2 forces WIN)\033[0m";
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
std::cout << "Move " << abs_move << ": " << result_str << "\n";
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
if (!has_moves) {
|
| 199 |
+
std::cout << "\n*** No valid moves. Game Over. ***\n";
|
| 200 |
+
break;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
std::cout << "--------------------------------------------------------\n";
|
| 204 |
+
int chosen_abs_move = -1;
|
| 205 |
+
int chosen_canonical = -1;
|
| 206 |
+
while (true) {
|
| 207 |
+
std::cout << "Enter move (" << start_move << "-" << end_move << ") or 'q' to quit: ";
|
| 208 |
+
std::string input;
|
| 209 |
+
if (!(std::cin >> input) || input == "q") {
|
| 210 |
+
std::cout << "Exiting Oracle.\n";
|
| 211 |
+
return 0;
|
| 212 |
+
}
|
| 213 |
+
try {
|
| 214 |
+
chosen_abs_move = std::stoi(input);
|
| 215 |
+
if (chosen_abs_move >= start_move && chosen_abs_move <= end_move) {
|
| 216 |
+
chosen_canonical = is_p1_turn ? chosen_abs_move : (chosen_abs_move - 5);
|
| 217 |
+
if (s.board[chosen_canonical] > 0) break;
|
| 218 |
+
}
|
| 219 |
+
} catch (...) {}
|
| 220 |
+
std::cout << "Invalid move. Try again.\n";
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Execute chosen move and update canonical state
|
| 224 |
+
bool empties;
|
| 225 |
+
ExecuteMoveAndFlip(s, chosen_canonical, s, empties);
|
| 226 |
+
|
| 227 |
+
if (empties || s.K_opp >= 26) {
|
| 228 |
+
std::cout << "\n========================================================\n";
|
| 229 |
+
std::cout << "*** GAME OVER: " << current_player << " Wins! ***\n";
|
| 230 |
+
std::cout << "========================================================\n";
|
| 231 |
+
break;
|
| 232 |
+
}
|
| 233 |
+
ply++;
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
else {
|
| 237 |
+
PrintUsage();
|
| 238 |
+
return 1;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return 0;
|
| 242 |
+
}
|
oracle_bridge.cpp
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include <pybind11/pybind11.h>
|
| 2 |
+
#include <pybind11/stl.h>
|
| 3 |
+
#include <random>
|
| 4 |
+
#include "BestemsheCore.h"
|
| 5 |
+
#include "StateIndex.h"
|
| 6 |
+
#include "Inference.h"
|
| 7 |
+
|
| 8 |
+
namespace py = pybind11;
|
| 9 |
+
using namespace Bestemshe;
|
| 10 |
+
|
| 11 |
+
class OracleBridge {
|
| 12 |
+
private:
|
| 13 |
+
InferenceEngine db;
|
| 14 |
+
std::mt19937_64 rng;
|
| 15 |
+
|
| 16 |
+
public:
|
| 17 |
+
OracleBridge() {
|
| 18 |
+
StateIndex::InitCombinatorics();
|
| 19 |
+
std::random_device rd;
|
| 20 |
+
rng.seed(rd());
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Returns a 14-element Python List: [P1_pits(5), P2_pits(5), K1, K2, PlyClock, OracleValue]
|
| 24 |
+
std::vector<int> sample_state(uint8_t M) {
|
| 25 |
+
uint64_t max_idx = StateIndex::GetLayerSize(M);
|
| 26 |
+
std::uniform_int_distribution<uint64_t> dist(0, max_idx - 1);
|
| 27 |
+
|
| 28 |
+
State s;
|
| 29 |
+
while (true) {
|
| 30 |
+
uint64_t rand_idx = dist(rng);
|
| 31 |
+
s = StateIndex::UnindexState(rand_idx, M);
|
| 32 |
+
|
| 33 |
+
// Ensure we don't sample a state where the game is already mathematically over
|
| 34 |
+
if (s.K_self <= 25 && s.K_opp <= 25) {
|
| 35 |
+
break;
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
std::vector<int> py_state(14, 0);
|
| 40 |
+
|
| 41 |
+
// P1 pits (0-4) and P2 pits (5-9)
|
| 42 |
+
for(int i = 0; i < 10; i++) {
|
| 43 |
+
py_state[i] = s.board[i];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
py_state[10] = s.K_self;
|
| 47 |
+
py_state[11] = s.K_opp;
|
| 48 |
+
py_state[12] = 0; // Reset Ply Clock for the AlphaZero rollout
|
| 49 |
+
|
| 50 |
+
// Fetch Absolute Ground Truth (Optional, but brilliant for debugging/metrics)
|
| 51 |
+
// 1 = P1 WIN, -1 = P2 WIN (P1 LOSS), 0 = DRAW, -99 = UNKNOWN (Not loaded)
|
| 52 |
+
uint64_t s_idx = StateIndex::IndexState(s);
|
| 53 |
+
GameValue val = db.query_state(M, s.K_opp, s_idx);
|
| 54 |
+
if (val == GameValue::WIN) py_state[13] = 1;
|
| 55 |
+
else if (val == GameValue::LOSS) py_state[13] = -1;
|
| 56 |
+
else if (val == GameValue::DRAW) py_state[13] = 0;
|
| 57 |
+
else py_state[13] = -99;
|
| 58 |
+
|
| 59 |
+
return py_state;
|
| 60 |
+
}
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
PYBIND11_MODULE(bestemshe_oracle, m) {
|
| 64 |
+
m.doc() = "Bestemshe C++ Oracle Bridge for RCL-Zero";
|
| 65 |
+
|
| 66 |
+
py::class_<OracleBridge>(m, "OracleBridge")
|
| 67 |
+
.def(py::init<>())
|
| 68 |
+
.def("sample_state", &OracleBridge::sample_state, "Samples a random non-terminal state from layer M");
|
| 69 |
+
}
|
packages.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
g++
|
| 2 |
+
make
|
| 3 |
+
libzstd-dev
|
query.cpp
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Tablebase Explorer CLI.
|
| 2 |
+
// Usage: ./query K1 K2 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9
|
| 3 |
+
// The 12 fields describe the canonical state from the side-to-move perspective
|
| 4 |
+
// (K1/p0-p4 = mover, K2/p5-p9 = opponent). Emits a single JSON object.
|
| 5 |
+
#include "StateIndex.h"
|
| 6 |
+
#include "Oracle.h"
|
| 7 |
+
#include <iostream>
|
| 8 |
+
#include <string>
|
| 9 |
+
#include <sstream>
|
| 10 |
+
|
| 11 |
+
using namespace Bestemshe;
|
| 12 |
+
|
| 13 |
+
static const char* value_str(OracleValue v) {
|
| 14 |
+
switch (v) {
|
| 15 |
+
case OracleValue::WIN: return "WIN";
|
| 16 |
+
case OracleValue::LOSS: return "LOSS";
|
| 17 |
+
case OracleValue::DRAW: return "DRAW";
|
| 18 |
+
default: return "UNKNOWN";
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
static void emit_error(const std::string& msg) {
|
| 23 |
+
std::cout << "{\"error\": \"" << msg << "\"}\n";
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Mirrors the sowing in ExecuteMoveAndFlip to find the pit (0..9) where the
|
| 27 |
+
// last stone lands. Used only for move notation.
|
| 28 |
+
static int landing_pit(const State& s, int i) {
|
| 29 |
+
int pieces = s.board[i];
|
| 30 |
+
int current = i;
|
| 31 |
+
if (pieces == 1) {
|
| 32 |
+
current = (current + 1) % 10;
|
| 33 |
+
} else {
|
| 34 |
+
pieces--;
|
| 35 |
+
while (pieces > 0) { current = (current + 1) % 10; pieces--; }
|
| 36 |
+
}
|
| 37 |
+
return current;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
static std::string board_json(const State& s) {
|
| 41 |
+
std::ostringstream o;
|
| 42 |
+
o << "[";
|
| 43 |
+
for (int i = 0; i < 10; ++i) o << (i ? "," : "") << static_cast<int>(s.board[i]);
|
| 44 |
+
o << "]";
|
| 45 |
+
return o.str();
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
int main(int argc, char* argv[]) {
|
| 49 |
+
StateIndex::InitCombinatorics();
|
| 50 |
+
|
| 51 |
+
if (argc != 13) {
|
| 52 |
+
emit_error("expected 12 integers: K1 K2 p0..p9");
|
| 53 |
+
return 1;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
int vals[12];
|
| 57 |
+
for (int i = 0; i < 12; ++i) {
|
| 58 |
+
try { vals[i] = std::stoi(argv[i + 1]); }
|
| 59 |
+
catch (...) { emit_error("non-numeric argument"); return 1; }
|
| 60 |
+
if (vals[i] < 0 || vals[i] > 50) { emit_error("field out of range 0..50"); return 1; }
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
State s;
|
| 64 |
+
s.K_self = static_cast<uint8_t>(vals[0]);
|
| 65 |
+
s.K_opp = static_cast<uint8_t>(vals[1]);
|
| 66 |
+
s.M = s.K_self + s.K_opp;
|
| 67 |
+
int pit_sum = 0;
|
| 68 |
+
for (int i = 0; i < 10; ++i) {
|
| 69 |
+
s.board[i] = static_cast<uint8_t>(vals[i + 2]);
|
| 70 |
+
pit_sum += vals[i + 2];
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if (pit_sum + s.M != 50) { emit_error("stones must total 50 (pits + kazans)"); return 1; }
|
| 74 |
+
if ((s.K_self % 2) || (s.K_opp % 2)) { emit_error("kazan counts must be even (captures are even)"); return 1; }
|
| 75 |
+
if (s.K_self > 24 || s.K_opp > 24) {
|
| 76 |
+
emit_error("game already decided (a kazan holds 26+) or kazan exceeds tablebase range");
|
| 77 |
+
return 1;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
TablebaseOracle oracle;
|
| 81 |
+
|
| 82 |
+
OracleValue pos_val = oracle.query(s);
|
| 83 |
+
if (pos_val == OracleValue::ERROR) {
|
| 84 |
+
emit_error("tablebase lookup failed (missing/corrupt layer files in " + oracle.dir() + ")");
|
| 85 |
+
return 1;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
std::ostringstream out;
|
| 89 |
+
out << "{\"position\": {\"K1\": " << (int)s.K_self << ", \"K2\": " << (int)s.K_opp
|
| 90 |
+
<< ", \"board\": " << board_json(s) << "},\n"
|
| 91 |
+
<< " \"value\": \"" << value_str(pos_val) << "\",\n"
|
| 92 |
+
<< " \"moves\": [";
|
| 93 |
+
|
| 94 |
+
bool first = true;
|
| 95 |
+
for (int pit = 0; pit < 5; ++pit) {
|
| 96 |
+
if (s.board[pit] == 0) continue; // illegal move: omit entirely
|
| 97 |
+
|
| 98 |
+
int land = landing_pit(s, pit);
|
| 99 |
+
int from_idx = pit + 1; // 1..5
|
| 100 |
+
int to_idx = (land % 5) + 1; // 1..5 (either side)
|
| 101 |
+
|
| 102 |
+
State child;
|
| 103 |
+
bool empties;
|
| 104 |
+
bool capture = ExecuteMoveAndFlip(s, pit, child, empties);
|
| 105 |
+
|
| 106 |
+
const char* result;
|
| 107 |
+
bool terminal = false;
|
| 108 |
+
if (empties || child.K_opp >= 26) {
|
| 109 |
+
// Terminal: the mover wins immediately.
|
| 110 |
+
result = "WIN";
|
| 111 |
+
terminal = true;
|
| 112 |
+
} else {
|
| 113 |
+
OracleValue v = oracle.query(child);
|
| 114 |
+
if (v == OracleValue::ERROR) { emit_error("lookup failed for move child"); return 1; }
|
| 115 |
+
// Child value is from the opponent's perspective; invert for the mover.
|
| 116 |
+
if (v == OracleValue::WIN) result = "LOSS";
|
| 117 |
+
else if (v == OracleValue::LOSS) result = "WIN";
|
| 118 |
+
else result = "DRAW";
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (!first) out << ",";
|
| 122 |
+
first = false;
|
| 123 |
+
out << "\n {\"from\": " << from_idx << ", \"to\": " << to_idx
|
| 124 |
+
<< ", \"capture\": " << (capture ? "true" : "false")
|
| 125 |
+
<< ", \"result\": \"" << result << "\""
|
| 126 |
+
<< ", \"terminal\": " << (terminal ? "true" : "false");
|
| 127 |
+
|
| 128 |
+
// Raw canonical child (opponent to move). Emitted even for terminal moves
|
| 129 |
+
// so the UI can show the final board before freezing the game.
|
| 130 |
+
out << ", \"child\": {\"K1\": " << (int)child.K_self
|
| 131 |
+
<< ", \"K2\": " << (int)child.K_opp << ", \"board\": " << board_json(child) << "}";
|
| 132 |
+
out << "}";
|
| 133 |
+
}
|
| 134 |
+
out << "\n]}";
|
| 135 |
+
|
| 136 |
+
std::cout << out.str() << "\n";
|
| 137 |
+
return 0;
|
| 138 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0
|
| 2 |
+
huggingface_hub
|
| 3 |
+
spaces
|
setup.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import pybind11
|
| 4 |
+
from setuptools import setup, Extension
|
| 5 |
+
|
| 6 |
+
# Locate pybind11 headers
|
| 7 |
+
pybind11_include = pybind11.get_include()
|
| 8 |
+
|
| 9 |
+
# Base directories
|
| 10 |
+
inc_dirs = [pybind11_include, '.']
|
| 11 |
+
lib_dirs = []
|
| 12 |
+
|
| 13 |
+
# Detect macOS and inject Apple Silicon Homebrew paths
|
| 14 |
+
if sys.platform == 'darwin':
|
| 15 |
+
print("[RCL-ZERO BUILD] macOS detected. Injecting Homebrew ARM64 paths...")
|
| 16 |
+
inc_dirs.append('/opt/homebrew/include')
|
| 17 |
+
lib_dirs.append('/opt/homebrew/lib')
|
| 18 |
+
|
| 19 |
+
ext_modules = [
|
| 20 |
+
Extension(
|
| 21 |
+
'bestemshe_oracle',
|
| 22 |
+
['oracle_bridge.cpp', 'Compressor.cpp'],
|
| 23 |
+
include_dirs=inc_dirs,
|
| 24 |
+
library_dirs=lib_dirs,
|
| 25 |
+
language='c++',
|
| 26 |
+
extra_compile_args=['-std=c++17', '-O3', '-Wall'],
|
| 27 |
+
extra_link_args=['-lzstd']
|
| 28 |
+
),
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
setup(
|
| 32 |
+
name='bestemshe_oracle',
|
| 33 |
+
version='1.0.0',
|
| 34 |
+
description='C++ HPC Oracle for Bestemshe',
|
| 35 |
+
ext_modules=ext_modules,
|
| 36 |
+
)
|
upload_tablebase.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chunked tablebase uploader for the HF Space.
|
| 2 |
+
|
| 3 |
+
Sorts layers/compressed by file size (largest first). Files >= BIG_THRESHOLD are
|
| 4 |
+
committed one per commit; smaller files are batched into groups of <= GROUP_BYTES.
|
| 5 |
+
Files already present on the Space are skipped, so the script is fully resumable —
|
| 6 |
+
just rerun it after any failure.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
from huggingface_hub import HfApi, CommitOperationAdd
|
| 13 |
+
|
| 14 |
+
REPO = "ansarzeinulla/bestemshe-tablebase"
|
| 15 |
+
REPO_TYPE = "dataset"
|
| 16 |
+
LOCAL_DIR = "layers/compressed"
|
| 17 |
+
BIG_THRESHOLD = 100 * 1024 * 1024 # 100MB: upload individually
|
| 18 |
+
GROUP_BYTES = 500 * 1024 * 1024 # small files: <=500MB per commit
|
| 19 |
+
|
| 20 |
+
api = HfApi()
|
| 21 |
+
|
| 22 |
+
existing = set(api.list_repo_files(REPO, repo_type=REPO_TYPE))
|
| 23 |
+
|
| 24 |
+
files = []
|
| 25 |
+
for name in os.listdir(LOCAL_DIR):
|
| 26 |
+
path = os.path.join(LOCAL_DIR, name)
|
| 27 |
+
if not os.path.isfile(path):
|
| 28 |
+
continue
|
| 29 |
+
repo_path = f"{LOCAL_DIR}/{name}"
|
| 30 |
+
if repo_path in existing:
|
| 31 |
+
continue
|
| 32 |
+
files.append((os.path.getsize(path), path, repo_path))
|
| 33 |
+
|
| 34 |
+
files.sort(reverse=True) # largest first
|
| 35 |
+
total = sum(f[0] for f in files)
|
| 36 |
+
print(f"[plan] {len(files)} files to upload, {total / 1e9:.2f} GB "
|
| 37 |
+
f"({len(existing)} repo files already present)", flush=True)
|
| 38 |
+
|
| 39 |
+
# Build commit groups: big files alone, small files batched.
|
| 40 |
+
groups, batch, batch_bytes = [], [], 0
|
| 41 |
+
for size, path, repo_path in files:
|
| 42 |
+
if size >= BIG_THRESHOLD:
|
| 43 |
+
groups.append([(size, path, repo_path)])
|
| 44 |
+
elif batch_bytes + size > GROUP_BYTES and batch:
|
| 45 |
+
groups.append(batch)
|
| 46 |
+
batch, batch_bytes = [(size, path, repo_path)], size
|
| 47 |
+
else:
|
| 48 |
+
batch.append((size, path, repo_path))
|
| 49 |
+
batch_bytes += size
|
| 50 |
+
if batch:
|
| 51 |
+
groups.append(batch)
|
| 52 |
+
|
| 53 |
+
done_bytes = 0
|
| 54 |
+
for i, group in enumerate(groups, 1):
|
| 55 |
+
ops = [CommitOperationAdd(path_in_repo=rp, path_or_fileobj=p) for _, p, rp in group]
|
| 56 |
+
gb = sum(s for s, _, _ in group) / 1e9
|
| 57 |
+
label = group[0][2] if len(group) == 1 else f"{len(group)} files"
|
| 58 |
+
print(f"[{i}/{len(groups)}] committing {label} ({gb:.2f} GB)...", flush=True)
|
| 59 |
+
api.create_commit(
|
| 60 |
+
repo_id=REPO, repo_type=REPO_TYPE, operations=ops,
|
| 61 |
+
commit_message=f"Tablebase upload {i}/{len(groups)}: {label}",
|
| 62 |
+
)
|
| 63 |
+
done_bytes += sum(s for s, _, _ in group)
|
| 64 |
+
print(f" done — {done_bytes / 1e9:.2f}/{total / 1e9:.2f} GB total", flush=True)
|
| 65 |
+
|
| 66 |
+
print("[SUCCESS] tablebase fully uploaded.")
|