krestiki-game / engine.cpp
Toofunoff's picture
Deploy production version of 5x5 solved Tic-Tac-Toe engine
1f7f6e3 verified
Raw
History Blame Contribute Delete
21.8 kB
#include <iostream>
#include <vector>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdint>
#include <cstddef>
#include <chrono>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <mutex>
#include <atomic>
#include <random>
using namespace std::chrono;
#if defined(__GNUC__) || defined(__clang__)
#define FORCE_INLINE __attribute__((always_inline)) inline
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#define CTZ(x) __builtin_ctz(x)
#else
#define FORCE_INLINE inline
#define UNLIKELY(x) (x)
static inline int CTZ(uint32_t v) {
int c = 0; if (!v) return 32;
while (!(v & 1)) { ++c; v >>= 1; } return c;
}
#endif
// ============================================================
// КОНСТАНТЫ И ГЛОБАЛЬНЫЕ СТРУКТУРЫ
// ============================================================
static const int TT_SIZE_BITS = 22; // 64 МБ RAM
static const int TT_SIZE = 1 << TT_SIZE_BITS;
static const int TT_MASK = TT_SIZE - 1;
static const int DTM_CACHE_SIZE_BITS = 19;
static const int DTM_CACHE_SIZE = 1 << DTM_CACHE_SIZE_BITS;
static const int DTM_CACHE_MASK = DTM_CACHE_SIZE - 1;
static const int TIME_LIMIT_MS = 2800;
static const int DTM_INF = 9999;
static uint64_t C[26][26];
static const int SYM_MAPS[8][25] = {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24},
{ 4, 9,14,19,24, 3, 8,13,18,23, 2, 7,12,17,22, 1, 6,11,16,21, 0, 5,10,15,20},
{24,23,22,21,20,19,18,17,16,15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
{20,15,10, 5, 0,21,16,11, 6, 1,22,17,12, 7, 2,23,18,13, 8, 3,24,19,14, 9, 4},
{20,21,22,23,24,15,16,17,18,19,10,11,12,13,14, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4},
{ 4, 3, 2, 1, 0, 9, 8, 7, 6, 5,14,13,12,11,10,19,18,17,16,15,24,23,22,21,20},
{ 0, 5,10,15,20, 1, 6,11,16,21, 2, 7,12,17,22, 3, 8,13,18,23, 4, 9,14,19,24},
{24,19,14, 9, 4,23,18,13, 8, 3,22,17,12, 7, 2,21,16,11, 6, 1,20,15,10, 5, 0}
};
static const uint32_t WIN_MASKS[28] = {
0x0000000f,0x0000001e,0x000001e0,0x000003c0,0x00003c00,0x00007800,
0x00078000,0x000f0000,0x00f00000,0x01e00000,0x00008421,0x00010842,
0x00021084,0x00042108,0x00084210,0x00108420,0x00210840,0x00421080,
0x00842100,0x01084200,0x00041041,0x00082082,0x00820820,0x01041040,
0x00008888,0x00011110,0x00111100,0x00222200
};
static const int ADJACENT_CORNERS[8] = {1, 5, 3, 9, 21, 15, 23, 19};
static const int CORNERS[4] = {0, 4, 20, 24};
alignas(64) static uint32_t SYM_BIT_LUT[8][25];
alignas(64) static uint32_t SYM_BYTE_LUT[8][3][256];
alignas(64) static uint32_t SYM_TOP_LUT[8][2];
static void build_sym_luts() {
for (int s = 0; s < 8; ++s) {
for (int i = 0; i < 25; ++i) SYM_BIT_LUT[s][i] = (1u << SYM_MAPS[s][i]);
for (int g = 0; g < 3; ++g) {
for (int v = 0; v < 256; ++v) {
uint32_t res = 0;
for (int b = 0; b < 8; ++b) if ((v >> b) & 1) res |= SYM_BIT_LUT[s][g * 8 + b];
SYM_BYTE_LUT[s][g][v] = res;
}
}
SYM_TOP_LUT[s][0] = 0;
SYM_TOP_LUT[s][1] = SYM_BIT_LUT[s][24];
}
}
static FORCE_INLINE uint32_t apply_sym_fast(uint32_t b, int s) {
return SYM_BYTE_LUT[s][0][ b & 0xFF]
| SYM_BYTE_LUT[s][1][(b >> 8) & 0xFF]
| SYM_BYTE_LUT[s][2][(b >> 16) & 0xFF]
| SYM_TOP_LUT [s] [(b >> 24) & 1 ];
}
static FORCE_INLINE uint64_t canonical_key(uint32_t xb, uint32_t ob) {
uint64_t best = ((uint64_t)xb << 25) | ob;
#pragma GCC unroll 7
for (int s = 1; s < 8; ++s) {
uint64_t k = ((uint64_t)apply_sym_fast(xb, s) << 25) | apply_sym_fast(ob, s);
if (k < best) best = k;
}
return best;
}
static FORCE_INLINE void get_canonical(uint32_t xb, uint32_t ob, uint32_t& bx, uint32_t& bo) {
bx = xb; bo = ob;
uint64_t bk = ((uint64_t)xb << 25) | ob;
#pragma GCC unroll 7
for (int s = 1; s < 8; ++s) {
uint32_t sx = apply_sym_fast(xb, s), so = apply_sym_fast(ob, s);
uint64_t k = ((uint64_t)sx << 25) | so;
if (k < bk) { bk = k; bx = sx; bo = so; }
}
}
// ============================================================
// ПОТОКОБЕЗОПАСНЫЕ КЭШИ (STRIPED LOCKS + ATOMICS)
// ============================================================
struct alignas(16) TTEntry {
uint64_t key;
float score;
int16_t depth;
uint64_t epoch;
};
struct alignas(16) DTMCacheEntry {
uint64_t key;
int16_t dist;
uint64_t epoch;
};
static TTEntry* TT = nullptr;
static DTMCacheEntry* s_dtm_cache = nullptr;
static std::mutex tt_locks[256];
static std::mutex dtm_locks[256];
static std::atomic<uint64_t> g_epoch{1};
static std::atomic<uint64_t> g_dtm_epoch{1};
// Пул отмен расширен до 1 000 000 слотов
static std::atomic<bool> g_cancel_flags[1000000];
static uint8_t* layers[26] = {};
static size_t layer_sizes[26] = {};
// ============================================================
// THREAD-LOCAL КОНТЕКСТ ПОИСКА
// ============================================================
thread_local int t_dtm_nodes = 0;
thread_local int t_dtm_node_limit = 50000;
thread_local int t_total_nodes = 0;
thread_local steady_clock::time_point t_start;
thread_local std::mt19937 rng(std::random_device{}());
static FORCE_INLINE bool check_time(int sid) {
if (sid >= 0 && sid < 1000000 && g_cancel_flags[sid].load(std::memory_order_relaxed)) return true;
if (UNLIKELY((++t_total_nodes & 1023) == 0)) {
if (duration_cast<milliseconds>(steady_clock::now() - t_start).count() >= TIME_LIMIT_MS) {
if (sid >= 0 && sid < 1000000) g_cancel_flags[sid].store(true, std::memory_order_relaxed);
return true;
}
}
return false;
}
// ============================================================
// ИНИЦИАЛИЗАЦИЯ И БД
// ============================================================
static void precompute() {
for (int i = 0; i < 26; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j) C[i][j] = C[i-1][j-1] + C[i-1][j];
}
build_sym_luts();
if (!TT) TT = new TTEntry[TT_SIZE]();
if (!s_dtm_cache) s_dtm_cache = new DTMCacheEntry[DTM_CACHE_SIZE]();
for(int i=0; i<1000000; ++i) g_cancel_flags[i].store(false);
}
static size_t get_expected_size(int t) {
if (t < 0 || t > 25) return 0;
uint64_t expected_states = (uint64_t)C[25][t] * C[t][(t + 1) / 2];
return (expected_states * 2 + 7) / 8;
}
static int load_layer_internal(int t, const char* path) {
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
struct stat sb;
if (fstat(fd, &sb) == 0 && sb.st_size > 0) {
if ((size_t)sb.st_size < get_expected_size(t)) {
close(fd); return 0;
}
layers[t] = (uint8_t*)mmap(nullptr, (size_t)sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (layers[t] != MAP_FAILED) {
layer_sizes[t] = sb.st_size;
if (t < 10) madvise(layers[t], sb.st_size, MADV_WILLNEED);
else madvise(layers[t], sb.st_size, MADV_RANDOM);
close(fd);
return 1;
}
}
close(fd);
return 0;
}
static uint64_t board_to_index(uint32_t xb, uint32_t ob) {
int t = __builtin_popcount(xb | ob);
int xc = __builtin_popcount(xb);
uint64_t idx_occ = 0, idx_x = 0;
uint32_t occ = xb | ob;
int i_occ = 0, i_x = 0;
while (occ) {
int bit = CTZ(occ);
idx_occ += C[bit][i_occ + 1];
if (xb & (1u << bit)) {
idx_x += C[i_occ][i_x + 1];
i_x++;
}
i_occ++;
occ &= occ - 1;
}
return idx_occ * C[t][xc] + idx_x;
}
static FORCE_INLINE int fetch_val_impl(int t, uint32_t xb, uint32_t ob) {
if (UNLIKELY(t < 0 || t > 25 || !layers[t])) return 0;
uint32_t cx, co;
get_canonical(xb, ob, cx, co);
uint64_t idx = board_to_index(cx, co);
return (layers[t][idx >> 2] >> ((idx & 3) << 1)) & 3;
}
static FORCE_INLINE bool check_win(uint32_t b) {
#pragma GCC unroll 28
for (int i = 0; i < 28; ++i)
if ((b & WIN_MASKS[i]) == WIN_MASKS[i]) return true;
return false;
}
// ============================================================
// DTM С ЗАЩИТОЙ ОТ КОЛЛИЗИЙ И УЛУЧШЕННЫМ СМЕШИВАНИЕМ
// ============================================================
static int dtm_max(uint32_t xb, uint32_t ob, int t, int opp, int sid);
static int dtm_min(uint32_t xb, uint32_t ob, int t, int p, int sid) {
if (check_time(sid)) return DTM_INF;
if (UNLIKELY(++t_dtm_nodes > t_dtm_node_limit)) return DTM_INF;
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) continue;
if (check_win(p==1 ? (xb|(1u<<i)) : (ob|(1u<<i)))) return 1;
}
if (t >= 24) return DTM_INF;
uint64_t key = canonical_key(xb, ob) ^ ((uint64_t)(p & 1) << 51) ^ (0ULL << 52);
uint64_t hash = key ^ (key >> 17) ^ (key >> 34);
int idx = (int)(hash & DTM_CACHE_MASK);
// ИСПРАВЛЕН БАГ 1: lock_idx вычисляется строго из пространства маски idx во избежание Data Race
int lock_idx = (idx >> 11) & 0xFF;
uint64_t current_epoch = g_dtm_epoch.load(std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(dtm_locks[lock_idx]);
if (s_dtm_cache[idx].key == key && s_dtm_cache[idx].epoch == current_epoch)
return s_dtm_cache[idx].dist;
}
int min_dist = DTM_INF;
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) continue;
uint32_t nx = (p==1) ? (xb|(1u<<i)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<i)) : ob;
if (fetch_val_impl(t+1, nx, no) != 2) continue;
int dist = dtm_max(nx, no, t+1, 3-p, sid);
if (dist < min_dist) {
min_dist = dist;
if (min_dist == 1) break;
}
}
int result = (min_dist >= DTM_INF) ? DTM_INF : min_dist + 1;
{
std::lock_guard<std::mutex> lock(dtm_locks[lock_idx]);
s_dtm_cache[idx].key = key;
s_dtm_cache[idx].dist = (int16_t)result;
s_dtm_cache[idx].epoch = current_epoch;
}
return result;
}
static int dtm_max(uint32_t xb, uint32_t ob, int t, int opp, int sid) {
if (check_time(sid)) return DTM_INF;
if (UNLIKELY(++t_dtm_nodes > t_dtm_node_limit)) return DTM_INF;
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) continue;
if (check_win(opp==1 ? (xb|(1u<<i)) : (ob|(1u<<i)))) return 1;
}
if (t >= 24) return 0;
uint64_t key = canonical_key(xb, ob) ^ ((uint64_t)(opp & 1) << 51) ^ (1ULL << 52);
uint64_t hash = key ^ (key >> 17) ^ (key >> 34);
int idx = (int)(hash & DTM_CACHE_MASK);
// ИСПРАВЛЕН БАГ 1: lock_idx вычисляется строго из пространства маски idx
int lock_idx = (idx >> 11) & 0xFF;
uint64_t current_epoch = g_dtm_epoch.load(std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(dtm_locks[lock_idx]);
if (s_dtm_cache[idx].key == key && s_dtm_cache[idx].epoch == current_epoch)
return s_dtm_cache[idx].dist;
}
int max_dist = -1;
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) continue;
uint32_t nx = (opp==1) ? (xb|(1u<<i)) : xb;
uint32_t no = (opp==2) ? (ob|(1u<<i)) : ob;
if (fetch_val_impl(t+1, nx, no) != 1) continue;
int dist = dtm_min(nx, no, t+1, 3-opp, sid);
if (dist > max_dist) max_dist = dist;
}
int result = (max_dist == -1) ? 0 : max_dist + 1;
{
std::lock_guard<std::mutex> lock(dtm_locks[lock_idx]);
s_dtm_cache[idx].key = key;
s_dtm_cache[idx].dist = (int16_t)result;
s_dtm_cache[idx].epoch = current_epoch;
}
return result;
}
static int dtm_best_move(uint32_t xb, uint32_t ob, int t, int p, const int* wins, int wcnt, int sid) {
g_dtm_epoch.fetch_add(1, std::memory_order_relaxed);
for (int i=0; i<wcnt; ++i) {
int m = wins[i];
if (check_win(p==1 ? (xb|(1u<<m)) : (ob|(1u<<m)))) return m;
}
int best_move = wins[0];
int min_path = DTM_INF;
for (int i=0; i<wcnt; ++i) {
int m = wins[i];
t_dtm_nodes = 0;
uint32_t nx = (p==1) ? (xb|(1u<<m)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<m)) : ob;
int path = dtm_max(nx, no, t+1, 3-p, sid);
if (path < min_path) {
min_path = path; best_move = m;
if (min_path <= 2) break;
}
}
return best_move;
}
// ============================================================
// EXPECTIMAX С STRIPED LOCKS
// ============================================================
static double trap_search(uint32_t xb, uint32_t ob, int t, int p, int opp, int depth_left, int sid) {
if (check_time(sid)) return 0.0;
if (t >= 25 || depth_left <= 0) return 0.0;
uint64_t key = canonical_key(xb, ob);
int tt_idx = (int)(key & TT_MASK);
int lock_idx = (tt_idx >> 14) & 0xFF; // Согласовано с маской TT_SIZE_BITS=22
uint64_t current_epoch = g_epoch.load(std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(tt_locks[lock_idx]);
if (TT[tt_idx].key == key && TT[tt_idx].epoch == current_epoch && TT[tt_idx].depth >= depth_left)
return TT[tt_idx].score;
}
int total_moves = 0;
double weight_sum = 0.0, future_sum = 0.0;
uint32_t fm = (~(xb | ob)) & 0x1FFFFFF;
while (fm) {
int i = CTZ(fm); fm &= fm - 1;
uint32_t ox = (opp == 1) ? (xb | (1u << i)) : xb;
uint32_t oo = (opp == 2) ? (ob | (1u << i)) : ob;
++total_moves;
int val = fetch_val_impl(t+1, ox, oo);
if (val == 1) {
t_dtm_nodes = 0;
int old_limit = t_dtm_node_limit;
t_dtm_node_limit = 200;
int dtm_dist = dtm_min(ox, oo, t+1, p, sid);
t_dtm_node_limit = old_limit;
if (dtm_dist >= DTM_INF) dtm_dist = 5;
weight_sum += 1.0 / (double)dtm_dist;
} else if (val == 0 && depth_left > 1) {
double best_our = 0.0;
uint32_t our_fm = (~(ox | oo)) & 0x1FFFFFF;
while (our_fm) {
int j = CTZ(our_fm); our_fm &= our_fm - 1;
uint32_t nx = (p==1) ? (ox|(1u<<j)) : ox;
uint32_t no = (p==2) ? (oo|(1u<<j)) : oo;
if (fetch_val_impl(t+2, nx, no) == 0) {
double fut = trap_search(nx, no, t+2, p, opp, depth_left-1, sid);
if (fut > best_our) best_our = fut;
if (sid >= 0 && sid < 1000000 && g_cancel_flags[sid].load(std::memory_order_relaxed)) break;
}
}
future_sum += best_our;
}
if (sid >= 0 && sid < 1000000 && g_cancel_flags[sid].load(std::memory_order_relaxed)) break;
}
if (total_moves == 0) return 0.0;
double result = (weight_sum + future_sum * 0.85) / (double)total_moves;
if (sid >= 0 && sid < 1000000 && !g_cancel_flags[sid].load(std::memory_order_relaxed)) {
std::lock_guard<std::mutex> lock(tt_locks[lock_idx]);
if (TT[tt_idx].epoch != current_epoch || TT[tt_idx].depth <= depth_left) {
TT[tt_idx].key = key;
TT[tt_idx].score = (float)result;
TT[tt_idx].depth = (int16_t)depth_left;
TT[tt_idx].epoch = current_epoch;
}
}
return result;
}
// ============================================================
// ВЫБОР ЛУЧШЕГО ХОДА
// ============================================================
static inline bool in_arr(const int* a, int n, int m) {
for (int k = 0; k < n; ++k) if (a[k] == m) return true;
return false;
}
static int get_best_move_internal(int t, uint32_t xb, uint32_t ob, int p, int ai_style, int sid) {
t_start = steady_clock::now();
t_total_nodes = 0;
if (sid >= 0 && sid < 1000000) g_cancel_flags[sid].store(false, std::memory_order_relaxed);
const int opp = 3 - p;
int wins[25], draws[25], loses[25];
int wcnt=0, dcnt=0, lcnt=0;
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) continue;
uint32_t nx = (p==1) ? (xb|(1u<<i)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<i)) : ob;
int val = fetch_val_impl(t+1, nx, no);
if (val == 2) wins[wcnt++] = i;
else if (val == 0) draws[dcnt++] = i;
else if (val == 1) loses[lcnt++] = i;
}
if (wcnt > 0) {
t_dtm_node_limit = 50000;
return dtm_best_move(xb, ob, t, p, wins, wcnt, sid);
}
if (dcnt > 0) {
// Удержание центра на ранних ходах
if (t == 0) {
int cands[25]; int c_cnt = 0;
for (int i=0; i<dcnt; ++i) if (in_arr(ADJACENT_CORNERS, 8, draws[i])) cands[c_cnt++] = draws[i];
if (c_cnt > 0) return cands[rng() % c_cnt];
for (int i=0; i<dcnt; ++i) if (in_arr(CORNERS, 4, draws[i])) cands[c_cnt++] = draws[i];
if (c_cnt > 0) return cands[rng() % c_cnt];
}
else if (t < 4) {
for (int i=0; i<dcnt; ++i) if (draws[i] == 12) return 12;
}
if (ai_style != 2) return draws[rng() % dcnt];
g_epoch.fetch_add(1, std::memory_order_relaxed);
g_dtm_epoch.fetch_add(1, std::memory_order_relaxed);
int best_move = draws[0];
double best_score = -1.0;
std::vector<std::pair<double, int>> ranked;
ranked.reserve(dcnt);
for (int i=0; i<dcnt; ++i) {
int m = draws[i];
uint32_t nx = (p==1) ? (xb|(1u<<m)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<m)) : ob;
ranked.push_back({trap_search(nx, no, t+1, p, opp, 1, sid), m});
}
std::partial_sort(ranked.begin(), ranked.begin() + 1, ranked.end(),
[](const auto& a, const auto& b){ return a.first > b.first; });
if (!ranked.empty()) { best_move = ranked[0].second; best_score = ranked[0].first; }
for (int depth = 2; sid >= 0 && sid < 1000000 && !g_cancel_flags[sid].load(std::memory_order_relaxed) && depth <= 20; ++depth) {
double iter_best = -1.0; int iter_move = best_move;
for (auto& [prev_sc, m] : ranked) {
if (g_cancel_flags[sid].load(std::memory_order_relaxed)) break;
uint32_t nx = (p==1) ? (xb|(1u<<m)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<m)) : ob;
double sc = trap_search(nx, no, t+1, p, opp, depth, sid);
prev_sc = sc;
if (sc > iter_best) { iter_best = sc; iter_move = m; }
}
if (!g_cancel_flags[sid].load(std::memory_order_relaxed)) {
best_move = iter_move; best_score = iter_best;
std::partial_sort(ranked.begin(), ranked.begin() + 1, ranked.end(),
[](const auto& a, const auto& b){ return a.first > b.first; });
}
if (best_score >= 1.0 || t + depth * 2 >= 24) break;
}
return best_move;
}
if (lcnt > 0) {
g_dtm_epoch.fetch_add(1, std::memory_order_relaxed);
t_dtm_node_limit = 50000;
int best_move = loses[0], max_path = -1;
for (int i=0; i<lcnt; ++i) {
int m = loses[i];
t_dtm_nodes = 0;
uint32_t nx = (p==1) ? (xb|(1u<<m)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<m)) : ob;
int path = dtm_min(nx, no, t+1, opp, sid);
if (path > max_path) { max_path = path; best_move = m; }
}
return best_move;
}
return -1;
}
static void get_cell_colors_internal(int t, uint32_t xb, uint32_t ob, int p, int ai_style, int* out, int sid) {
const int opp = 3 - p;
if (ai_style == 2) {
g_epoch.fetch_add(1, std::memory_order_relaxed);
g_dtm_epoch.fetch_add(1, std::memory_order_relaxed);
}
t_start = steady_clock::now();
t_total_nodes = 0;
if (sid >= 0 && sid < 1000000) g_cancel_flags[sid].store(false, std::memory_order_relaxed);
for (int i = 0; i < 25; ++i) {
if ((xb | ob) & (1u << i)) { out[i] = -1; continue; }
uint32_t nx = (p==1) ? (xb|(1u<<i)) : xb;
uint32_t no = (p==2) ? (ob|(1u<<i)) : ob;
int val = fetch_val_impl(t+1, nx, no);
if (val == 2) out[i] = 2;
else if (val == 1) out[i] = 1;
else if (ai_style == 2) {
double sc = trap_search(nx, no, t+1, p, opp, 2, sid);
out[i] = (sc > 0.0) ? 3 : 4;
} else out[i] = 4;
}
}
extern "C" {
void init_engine() { precompute(); }
int load_layer(int t, const char* path) { return load_layer_internal(t, path); }
void unload_layers() {
for (int i = 0; i < 26; ++i) {
if (layers[i]) { munmap(layers[i], layer_sizes[i]); layers[i] = nullptr; }
}
}
int fetch_val(int t, uint32_t xb, uint32_t ob) { return fetch_val_impl(t, xb, ob); }
int get_best_move(int t, uint32_t xb, uint32_t ob, int p, int ai_style, int sid) {
return get_best_move_internal(t, xb, ob, p, ai_style, sid);
}
void get_cell_colors(int t, uint32_t xb, uint32_t ob, int p, int ai_style, int* out_colors, int sid) {
get_cell_colors_internal(t, xb, ob, p, ai_style, out_colors, sid);
}
void abort_search(int sid) {
if (sid >= 0 && sid < 1000000) g_cancel_flags[sid].store(true, std::memory_order_relaxed);
}
int check_win_export(uint32_t mask) { return check_win(mask) ? 1 : 0; }
}