690 lines
17 KiB
C++
690 lines
17 KiB
C++
/*
|
|
This file is part of Sanmill.
|
|
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
|
|
|
|
Sanmill is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Sanmill is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <cassert>
|
|
|
|
#include <algorithm> // For std::count
|
|
#include <iomanip>
|
|
#include "movegen.h"
|
|
#include "search.h"
|
|
#include "thread.h"
|
|
#include "uci.h"
|
|
#include "tt.h"
|
|
#include "option.h"
|
|
|
|
#ifdef FLUTTER_UI
|
|
#include "engine_main.h"
|
|
#endif
|
|
|
|
ThreadPool Threads; // Global object
|
|
|
|
#ifdef OPENING_BOOK
|
|
#include <deque>
|
|
using namespace std;
|
|
#endif
|
|
|
|
#if _MSC_VER >= 1600
|
|
#pragma warning(disable:4695)
|
|
#pragma execution_character_set("ANSI")
|
|
#endif
|
|
|
|
|
|
/// Thread constructor launches the thread and waits until it goes to sleep
|
|
/// in idle_loop(). Note that 'searching' and 'exit' should be already set.
|
|
|
|
Thread::Thread(size_t n
|
|
#ifdef QT_GUI_LIB
|
|
, QObject *parent
|
|
#endif
|
|
) :
|
|
#ifdef QT_GUI_LIB
|
|
QObject(parent),
|
|
#endif
|
|
idx(n), stdThread(&Thread::idle_loop, this),
|
|
timeLimit(3600)
|
|
{
|
|
wait_for_search_finished();
|
|
}
|
|
|
|
|
|
/// Thread destructor wakes up the thread in idle_loop() and waits
|
|
/// for its termination. Thread should be already waiting.
|
|
|
|
Thread::~Thread()
|
|
{
|
|
assert(!searching);
|
|
|
|
exit = true;
|
|
start_searching();
|
|
stdThread.join();
|
|
}
|
|
|
|
/// Thread::clear() reset histories, usually before a new game
|
|
|
|
void Thread::clear() noexcept
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
/// Thread::start_searching() wakes up the thread that will start the search
|
|
|
|
void Thread::start_searching()
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
searching = true;
|
|
cv.notify_one(); // Wake up the thread in idle_loop()
|
|
}
|
|
|
|
void Thread::pause()
|
|
{
|
|
// TODO: Can work?
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
searching = false;
|
|
cv.notify_one(); // Wake up the thread in idle_loop()
|
|
}
|
|
|
|
/// Thread::wait_for_search_finished() blocks on the condition variable
|
|
/// until the thread has finished searching.
|
|
|
|
void Thread::wait_for_search_finished()
|
|
{
|
|
std::unique_lock<std::mutex> lk(mutex);
|
|
cv.wait(lk, [&] { return !searching; });
|
|
}
|
|
|
|
|
|
/// Thread::idle_loop() is where the thread is parked, blocked on the
|
|
/// condition variable, when it has no work to do.
|
|
|
|
void Thread::idle_loop()
|
|
{
|
|
//bestvalue = lastvalue = VALUE_ZERO;
|
|
|
|
while (true) {
|
|
std::unique_lock<std::mutex> lk(mutex);
|
|
searching = false;
|
|
|
|
cv.notify_one(); // Wake up anyone waiting for search finished
|
|
cv.wait(lk, [&] { return searching; });
|
|
|
|
if (exit)
|
|
return;
|
|
|
|
lk.unlock();
|
|
|
|
// TODO: Stockfish doesn't have this
|
|
if (rootPos == nullptr || rootPos->side_to_move() != us) {
|
|
continue;
|
|
}
|
|
|
|
clearTT();
|
|
|
|
#ifdef OPENING_BOOK
|
|
// gameOptions.getOpeningBook()
|
|
if (!openingBookDeque.empty()) {
|
|
char obc[16] = { 0 };
|
|
sq2str(obc);
|
|
strCommand = obc;
|
|
emitCommand();
|
|
} else {
|
|
#endif
|
|
int ret = search();
|
|
|
|
if (ret == 3 || ret == 50) {
|
|
loggerDebug("Draw\n\n");
|
|
strCommand = "draw";
|
|
emitCommand();
|
|
} else {
|
|
strCommand = nextMove();
|
|
if (strCommand != "" && strCommand != "error!") {
|
|
emitCommand();
|
|
}
|
|
}
|
|
#ifdef OPENING_BOOK
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
void Thread::setAi(Position *p)
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex);
|
|
|
|
this->rootPos = p;
|
|
|
|
#ifdef TRANSPOSITION_TABLE_ENABLE
|
|
#ifdef CLEAR_TRANSPOSITION_TABLE
|
|
TranspositionTable::clear();
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
void Thread::setAi(Position *p, int tl)
|
|
{
|
|
setAi(p);
|
|
|
|
timeLimit = tl;
|
|
}
|
|
|
|
void Thread::emitCommand()
|
|
{
|
|
#ifdef QT_GUI_LIB
|
|
emit command(strCommand);
|
|
#else
|
|
sync_cout << "bestmove " << strCommand.c_str();
|
|
std::cout << sync_endl;
|
|
|
|
#ifdef FLUTTER_UI
|
|
println("bestmove %s", strCommand.c_str());
|
|
#endif
|
|
|
|
#ifdef UCI_DO_BEST_MOVE
|
|
rootPos->command(strCommand.c_str());
|
|
us = rootPos->side_to_move();
|
|
#endif
|
|
|
|
#ifdef ANALYZE_POSITION
|
|
analyze(rootPos->side_to_move());
|
|
#endif
|
|
#endif // QT_GUI_LIB
|
|
}
|
|
|
|
#ifdef OPENING_BOOK
|
|
deque<int> openingBookDeque(
|
|
{
|
|
/* B W */
|
|
21, 23,
|
|
19, 20,
|
|
17, 18,
|
|
15,
|
|
}
|
|
);
|
|
|
|
deque<int> openingBookDequeBak;
|
|
|
|
void sq2str(char *str)
|
|
{
|
|
int sq = openingBookDeque.front();
|
|
openingBookDeque.pop_front();
|
|
openingBookDequeBak.push_back(sq);
|
|
|
|
File file = FILE_A;
|
|
Rank rank = RANK_1;
|
|
int sig = 1;
|
|
|
|
if (sq < 0) {
|
|
sq = -sq;
|
|
sig = 0;
|
|
}
|
|
|
|
file = file_of(sq);
|
|
rank = rank_of(sq);
|
|
|
|
if (sig == 1) {
|
|
snprintf(str, Position::RECORD_LEN_MAX, 16, "(%d,%d)", file, rank);
|
|
} else {
|
|
snprintf(str, Position::RECORD_LEN_MAX, "-(%d,%d)", file, rank);
|
|
}
|
|
}
|
|
#endif // OPENING_BOOK
|
|
|
|
void Thread::analyze(Color c)
|
|
{
|
|
static float nbwin = 0;
|
|
static float nwwin = 0;
|
|
static float ndraw = 0;
|
|
#ifndef QT_GUI_LIB
|
|
float total;
|
|
float bwinrate, wwinrate, drawrate;
|
|
#endif // !QT_GUI_LIB
|
|
|
|
const int d = (int)originDepth;
|
|
const int v = (int)bestvalue;
|
|
const int lv = (int)lastvalue;
|
|
const bool win = v >= VALUE_MATE;
|
|
const bool lose = v <= -VALUE_MATE;
|
|
const int np = v / VALUE_EACH_PIECE;
|
|
|
|
string strUs = (c == BLACK ? "Black" : "White");
|
|
string strThem = (c == BLACK ? "White" : "Black");
|
|
|
|
loggerDebug("Depth: %d\n\n", adjustedDepth);
|
|
|
|
const Position *p = rootPos;
|
|
|
|
cout << *p << "\n" << endl;
|
|
cout << std::dec;
|
|
|
|
switch (p->get_phase()) {
|
|
case Phase::placing:
|
|
cout << "Placing phrase" << endl;
|
|
break;
|
|
case Phase::moving:
|
|
cout << "Moving phase" << endl;
|
|
break;
|
|
case Phase::gameOver:
|
|
if (p->get_winner() == DRAW) {
|
|
cout << "Draw" << endl;
|
|
ndraw += 0.5; // TODO
|
|
} else if (p->get_winner() == BLACK) {
|
|
cout << "Black wins" << endl;
|
|
nbwin += 0.5; // TODO
|
|
} else if (p->get_winner() == WHITE) {
|
|
cout << "White wins" << endl;
|
|
nwwin += 0.5; // TODO
|
|
}
|
|
goto out;
|
|
break;
|
|
case Phase::none:
|
|
cout << "None phase" << endl;
|
|
break;
|
|
default:
|
|
cout << "Known phase" << endl;
|
|
}
|
|
|
|
if (v == VALUE_UNIQUE) {
|
|
cout << "Unique move" << endl << endl << endl;
|
|
return;
|
|
}
|
|
|
|
if (lv < -VALUE_EACH_PIECE && v == 0) {
|
|
cout << strThem << " made a bad move, " << strUs << "pulled back the balance of power!" << endl;
|
|
}
|
|
|
|
if (lv < 0 && v > 0) {
|
|
cout << strThem << " made a bad move, " << strUs << "reversed the situation!" << endl;
|
|
}
|
|
|
|
if (lv == 0 && v > VALUE_EACH_PIECE) {
|
|
cout << strThem << "Bad move!" << endl;
|
|
}
|
|
|
|
if (lv > VALUE_EACH_PIECE && v == 0) {
|
|
cout << strThem << "Good move, pulled back the balance of power" << endl;
|
|
}
|
|
|
|
if (lv > 0 && v < 0) {
|
|
cout << strThem << "Good move, reversed the situation!" << endl;
|
|
}
|
|
|
|
if (lv == 0 && v < -VALUE_EACH_PIECE) {
|
|
cout << strThem << "made a good move!" << endl;
|
|
}
|
|
|
|
if (lv != v) {
|
|
if (lv < 0 && v < 0) {
|
|
if (abs(lv) < abs(v)) {
|
|
cout << strThem << " has expanded its lead" << endl;
|
|
} else if (abs(lv) > abs(v)) {
|
|
cout << strThem << " has narrowed its lead" << endl;
|
|
}
|
|
}
|
|
|
|
if (lv > 0 && v > 0) {
|
|
if (abs(lv) < abs(v)) {
|
|
cout << strThem << " has expanded its lead" << endl;
|
|
} else if (abs(lv) > abs(v)) {
|
|
cout << strThem << " has narrowed its backwardness" << endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (win) {
|
|
cout << strThem << " will lose in " << d << " moves!" << endl;
|
|
} else if (lose) {
|
|
cout << strThem << " will win in " << d << " moves!" << endl;
|
|
} else if (np == 0) {
|
|
cout << "The two sides will maintain a balance of power after " << d << " moves" << endl;
|
|
} else if (np > 0) {
|
|
cout << strThem << " after " << d << " moves will backward " << np << " pieces" << endl;
|
|
} else if (np < 0) {
|
|
cout << strThem << " after " << d << " moves will lead " << -np << " pieces" << endl;
|
|
}
|
|
|
|
if (p->side_to_move() == BLACK) {
|
|
cout << "Black to move" << endl;
|
|
} else {
|
|
cout << "White to move" << endl;
|
|
}
|
|
|
|
#ifndef QT_GUI_LIB
|
|
total = nbwin + nwwin + ndraw;
|
|
|
|
if (total < 0.01) {
|
|
bwinrate = 0;
|
|
wwinrate = 0;
|
|
drawrate = 0;
|
|
} else {
|
|
bwinrate = (float)nbwin * 100 / total;
|
|
wwinrate = (float)nwwin * 100 / total;
|
|
drawrate = (float)ndraw * 100 / total;
|
|
}
|
|
|
|
cout << "Score: " << (int)nbwin << " : " << (int)nwwin << " : " << (int)ndraw << "\ttotal: " << (int)total << endl;
|
|
cout << fixed << setprecision(2) << bwinrate << "% : " << wwinrate << "% : " << drawrate << "%" << endl;
|
|
#endif // !QT_GUI_LIB
|
|
|
|
out:
|
|
cout << endl << endl;
|
|
}
|
|
|
|
Depth Thread::adjustDepth()
|
|
{
|
|
Depth d = 0;
|
|
|
|
#ifdef _DEBUG
|
|
constexpr Depth reduce = 0;
|
|
#else
|
|
Depth reduce = 0;
|
|
#endif
|
|
|
|
const Depth placingDepthTable_12[] = {
|
|
+1, 2, +2, 4, /* 0 ~ 3 */
|
|
+4, 12, +12, 18, /* 4 ~ 7 */
|
|
+12, 16, +16, 16, /* 8 ~ 11 */
|
|
+16, 16, +16, 17, /* 12 ~ 15 */
|
|
+17, 16, +16, 15, /* 16 ~ 19 */
|
|
+15, 14, +14, 14, /* 20 ~ 23 */
|
|
+14 /* 24 */
|
|
};
|
|
|
|
const Depth placingDepthTable_9[] = {
|
|
+1, 7, +7, 10, /* 0 ~ 3 */
|
|
+10, 12, +12, 12, /* 4 ~ 7 */
|
|
+12, 13, +13, 13, /* 8 ~ 11 */
|
|
+13, 13, +13, 13, /* 12 ~ 15 */
|
|
+13, 13, +13, /* 16 ~ 18 */
|
|
+13 /* 19 */
|
|
};
|
|
|
|
const Depth movingDepthTable[] = {
|
|
1, 1, 1, 1, /* 0 ~ 3 */
|
|
1, 1, 11, 11, /* 4 ~ 7 */
|
|
11, 11, 11, 11, /* 8 ~ 11 */
|
|
11, 11, 11, 11, /* 12 ~ 15 */
|
|
11, 11, 11, 11, /* 16 ~ 19 */
|
|
12, 12, 13, 14, /* 20 ~ 23 */
|
|
};
|
|
|
|
#ifdef ENDGAME_LEARNING
|
|
const Depth movingDiffDepthTable[] = {
|
|
0, 0, 0, /* 0 ~ 2 */
|
|
0, 0, 0, 0, 0, /* 3 ~ 7 */
|
|
0, 0, 0, 0, 0 /* 8 ~ 12 */
|
|
};
|
|
#else
|
|
const Depth movingDiffDepthTable[] = {
|
|
0, 0, 0, /* 0 ~ 2 */
|
|
11, 11, 10, 9, 8, /* 3 ~ 7 */
|
|
7, 6, 5, 4, 3 /* 8 ~ 12 */
|
|
};
|
|
#endif /* ENDGAME_LEARNING */
|
|
|
|
constexpr Depth flyingDepth = 9;
|
|
|
|
if (rootPos->phase == Phase::placing) {
|
|
const int index = rule.piecesCount * 2 - rootPos->count<IN_HAND>(BLACK) - rootPos->count<IN_HAND>(WHITE);
|
|
|
|
if (rule.piecesCount == 12) {
|
|
assert(0 <= index && index <= 24);
|
|
d = placingDepthTable_12[index];
|
|
} else {
|
|
assert(0 <= index && index <= 19);
|
|
d = placingDepthTable_9[index];
|
|
}
|
|
}
|
|
|
|
if (rootPos->phase == Phase::moving) {
|
|
const int pb = rootPos->count<ON_BOARD>(BLACK);
|
|
const int pw = rootPos->count<ON_BOARD>(WHITE);
|
|
|
|
const int pieces = pb + pw;
|
|
int diff = pb - pw;
|
|
|
|
if (diff < 0) {
|
|
diff = -diff;
|
|
}
|
|
|
|
d = movingDiffDepthTable[diff];
|
|
|
|
if (d == 0) {
|
|
d = movingDepthTable[pieces];
|
|
}
|
|
|
|
// Can fly
|
|
if (rule.mayFly) {
|
|
if (pb == rule.piecesAtLeastCount ||
|
|
pw == rule.piecesAtLeastCount) {
|
|
d = flyingDepth;
|
|
}
|
|
|
|
if (pb == rule.piecesAtLeastCount &&
|
|
pw == rule.piecesAtLeastCount) {
|
|
d = flyingDepth / 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unlikely(d > reduce)) {
|
|
d -= reduce;
|
|
}
|
|
|
|
d += DEPTH_ADJUST;
|
|
|
|
d = d >= 1 ? d : 1;
|
|
|
|
#if defined(FIX_DEPTH)
|
|
d = FIX_DEPTH;
|
|
#endif
|
|
|
|
assert(d <= 32);
|
|
|
|
//loggerDebug("Depth: %d\n", d);
|
|
|
|
return d;
|
|
}
|
|
|
|
void Thread::clearTT()
|
|
{
|
|
if (strcmp(rule.name, rule.name) != 0) {
|
|
#ifdef TRANSPOSITION_TABLE_ENABLE
|
|
TranspositionTable::clear();
|
|
#endif // TRANSPOSITION_TABLE_ENABLE
|
|
}
|
|
}
|
|
|
|
string Thread::nextMove()
|
|
{
|
|
#if 0
|
|
char charSelect = '*';
|
|
|
|
Position::print_board();
|
|
|
|
int moveIndex = 0;
|
|
bool foundBest = false;
|
|
|
|
int cs = root->childrenSize;
|
|
for (int i = 0; i < cs; i++) {
|
|
if (root->children[i]->move != bestMove) {
|
|
charSelect = ' ';
|
|
} else {
|
|
charSelect = '*';
|
|
foundBest = true;
|
|
}
|
|
|
|
loggerDebug("[%.2d] %d\t%s\t%d\t%u %c\n", moveIndex,
|
|
root->children[i]->move,
|
|
UCI::move(root->children[i]->move).c_str();
|
|
root->children[i]->value,
|
|
0,
|
|
charSelect);
|
|
|
|
moveIndex++;
|
|
}
|
|
|
|
Color side = position->sideToMove;
|
|
|
|
#ifdef ENDGAME_LEARNING
|
|
// Check if very weak
|
|
if (gameOptions.isEndgameLearningEnabled()) {
|
|
if (bestValue <= -VALUE_KNOWN_WIN) {
|
|
Endgame endgame;
|
|
endgame.type = state->position->playerSideToMove == PLAYER_BLACK ?
|
|
whiteWin : blackWin;
|
|
Key endgameHash = position->key(); // TODO: Do not generate hash repeately
|
|
saveEndgameHash(endgameHash, endgame);
|
|
}
|
|
}
|
|
#endif /* ENDGAME_LEARNING */
|
|
|
|
if (gameOptions.getResignIfMostLose() == true) {
|
|
if (root->value <= -VALUE_MATE) {
|
|
gameOverReason = loseReasonResign;
|
|
//snprintf(record, Position::RECORD_LEN_MAX, "Player%d give up!", position->sideToMove);
|
|
return record;
|
|
}
|
|
}
|
|
|
|
nodeCount = 0;
|
|
|
|
#endif
|
|
|
|
#ifdef TRANSPOSITION_TABLE_ENABLE
|
|
#ifdef TRANSPOSITION_TABLE_DEBUG
|
|
size_t hashProbeCount = ttHitCount + ttMissCount;
|
|
if (hashProbeCount) {
|
|
loggerDebug("[posKey] probe: %llu, hit: %llu, miss: %llu, hit rate: %llu%%\n",
|
|
hashProbeCount, ttHitCount, ttMissCount, ttHitCount * 100 / hashProbeCount);
|
|
}
|
|
#endif // TRANSPOSITION_TABLE_DEBUG
|
|
#endif // TRANSPOSITION_TABLE_ENABLE
|
|
|
|
#if 0
|
|
if (foundBest == false) {
|
|
loggerDebug("Warning: Best Move NOT Found\n");
|
|
}
|
|
#endif
|
|
|
|
return UCI::move(bestMove);
|
|
}
|
|
|
|
#ifdef ENDGAME_LEARNING
|
|
bool Thread::probeEndgameHash(Key posKey, Endgame &endgame)
|
|
{
|
|
return endgameHashMap.find(posKey, endgame);
|
|
}
|
|
|
|
int Thread::saveEndgameHash(Key posKey, const Endgame &endgame)
|
|
{
|
|
Key hashValue = endgameHashMap.insert(posKey, endgame);
|
|
unsigned addr = hashValue * (sizeof(posKey) + sizeof(endgame));
|
|
|
|
loggerDebug("[endgame] Record 0x%08I32x (%d) to Endgame hash map, TTEntry: 0x%08I32x, Address: 0x%08I32x\n",
|
|
posKey, endgame.type, hashValue, addr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void Thread::clearEndgameHashMap()
|
|
{
|
|
endgameHashMap.clear();
|
|
}
|
|
|
|
void Thread::saveEndgameHashMapToFile()
|
|
{
|
|
const string filename = "endgame.txt";
|
|
endgameHashMap.dump(filename);
|
|
|
|
loggerDebug("[endgame] Dump hash map to file\n");
|
|
}
|
|
|
|
void Thread::loadEndgameFileToHashMap()
|
|
{
|
|
const string filename = "endgame.txt";
|
|
endgameHashMap.load(filename);
|
|
}
|
|
|
|
#endif // ENDGAME_LEARNING
|
|
|
|
/// ThreadPool::set() creates/destroys threads to match the requested number.
|
|
/// Created and launched threads will immediately go to sleep in idle_loop.
|
|
/// Upon resizing, threads are recreated to allow for binding if necessary.
|
|
|
|
void ThreadPool::set(size_t requested)
|
|
{
|
|
if (size() > 0) { // destroy any existing thread(s)
|
|
main()->wait_for_search_finished();
|
|
|
|
while (size() > 0)
|
|
delete back(), pop_back();
|
|
}
|
|
|
|
if (requested > 0) { // create new thread(s)
|
|
push_back(new MainThread(0));
|
|
|
|
while (size() < requested)
|
|
push_back(new Thread(size()));
|
|
clear();
|
|
|
|
#ifdef TRANSPOSITION_TABLE_ENABLE
|
|
// Reallocate the hash with the new threadpool size
|
|
TT.resize(size_t(Options["Hash"]));
|
|
#endif
|
|
|
|
// Init thread number dependent search params.
|
|
Search::init();
|
|
}
|
|
}
|
|
|
|
/// ThreadPool::clear() sets threadPool data to initial values.
|
|
|
|
void ThreadPool::clear()
|
|
{
|
|
for (Thread *th : *this)
|
|
th->clear();
|
|
}
|
|
|
|
|
|
/// ThreadPool::start_thinking() wakes up main thread waiting in idle_loop() and
|
|
/// returns immediately. Main thread will wake up other threads and start the search.
|
|
|
|
void ThreadPool::start_thinking(Position *pos, bool ponderMode)
|
|
{
|
|
main()->wait_for_search_finished();
|
|
|
|
main()->stopOnPonderhit = stop = false;
|
|
increaseDepth = true;
|
|
main()->ponder = ponderMode;
|
|
|
|
// We use Position::set() to set root position across threads. But there are
|
|
// some StateInfo fields (previous, pliesFromNull, capturedPiece) that cannot
|
|
// be deduced from a fen string, so set() clears them and they are set from
|
|
// setupStates->back() later. The rootState is per thread, earlier states are shared
|
|
// since they are read-only.
|
|
for (Thread *th : *this) {
|
|
// TODO
|
|
//th->rootPos->set(pos->fen(), &setupStates->back(), th);
|
|
th->rootPos = pos;
|
|
}
|
|
|
|
main()->start_searching();
|
|
}
|