Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ __pycache__
*.cbp

tmp*.txt

test/
.vscode/
cpp/write
cpp/runtests
cpp/example
Expand Down
42 changes: 34 additions & 8 deletions cpp/command/gtp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ static const vector<string> knownCommands = {
// "analyze",
"lz-analyze",
"kata-analyze",

"kata-problem_analyze",
//Display raw neural net evaluations
"kata-raw-nn",

Expand Down Expand Up @@ -585,7 +585,12 @@ struct GTPEngine {
int minMoves = 0;
int maxMoves = 10000000;
bool showOwnership = false;
bool isProblemAnalyze = false;
Loc problemAnalyzeTopLeftCorner = Board::NULL_LOC;
Loc problemAnalyzeBottomRightCorner = Board::NULL_LOC;

double secondsPerReport = 1e30;

};

std::function<void(const Search* search)> getAnalyzeCallback(Player pla, AnalyzeArgs args) {
Expand Down Expand Up @@ -1010,7 +1015,16 @@ struct GTPEngine {
bot->setAlwaysIncludeOwnerMap(true);
else
bot->setAlwaysIncludeOwnerMap(false);

if (args.isProblemAnalyze) {
bot->setProblemAnalyze(true);
bot->setProblemAnalyzeTopLeftCorner(args.problemAnalyzeTopLeftCorner);
bot->setProblemAnalyzeBottomRightCorner(args.problemAnalyzeBottomRightCorner);
} else {
bot->setProblemAnalyze(false);
bot->setProblemAnalyzeTopLeftCorner(Board::NULL_LOC);
bot->setProblemAnalyzeBottomRightCorner(Board::NULL_LOC);
}

double searchFactor = 1e40; //go basically forever
bot->analyze(pla, searchFactor, args.secondsPerReport, callback);
}
Expand Down Expand Up @@ -1154,16 +1168,20 @@ struct GTPEngine {


//User should pre-fill pla with a default value, as it will not get filled in if the parsed command doesn't specify
static GTPEngine::AnalyzeArgs parseAnalyzeCommand(const string& command, const vector<string>& pieces, Player& pla, bool& parseFailed) {
static GTPEngine::AnalyzeArgs parseAnalyzeCommand(const string& command, const vector<string>& pieces, GTPEngine* engine, bool& parseFailed) {
Player pla = engine->bot->getRootPla();
Board board = engine->bot->getRootBoard();

int numArgsParsed = 0;

bool isLZ = (command == "lz-analyze" || command == "lz-genmove_analyze");
bool isKata = (command == "kata-analyze" || command == "kata-genmove_analyze");
bool isKata = (command == "kata-analyze" || command == "kata-genmove_analyze" || command == "kata-problem_analyze");
double lzAnalyzeInterval = 1e30;
int minMoves = 0;
int maxMoves = 10000000;
bool showOwnership = false;

Loc problemAnalyzeTopLeftCorner = Board::NULL_LOC;
Loc problemAnalyzeBottomRightCorner = Board::NULL_LOC;
parseFailed = false;

//Format:
Expand Down Expand Up @@ -1228,6 +1246,10 @@ static GTPEngine::AnalyzeArgs parseAnalyzeCommand(const string& command, const v
}
else if(isKata && key == "ownership" && Global::tryStringToBool(value,showOwnership)) {
continue;
} else if(isKata && key == "topleft" && Location::tryOfString(value, board, problemAnalyzeTopLeftCorner)) {
continue;
} else if(isKata && key == "bottomright" && Location::tryOfString(value, board, problemAnalyzeBottomRightCorner)) {
continue;
}

parseFailed = true;
Expand All @@ -1243,6 +1265,10 @@ static GTPEngine::AnalyzeArgs parseAnalyzeCommand(const string& command, const v
args.minMoves = minMoves;
args.maxMoves = maxMoves;
args.showOwnership = showOwnership;
args.problemAnalyzeTopLeftCorner = problemAnalyzeTopLeftCorner;
args.problemAnalyzeBottomRightCorner = problemAnalyzeBottomRightCorner;
args.isProblemAnalyze = command == "kata-problem_analyze";

return args;
}

Expand Down Expand Up @@ -1999,7 +2025,7 @@ int MainCmds::gtp(int argc, const char* const* argv) {
else if(command == "genmove_analyze" || command == "lz-genmove_analyze" || command == "kata-genmove_analyze") {
Player pla = engine->bot->getRootPla();
bool parseFailed = false;
GTPEngine::AnalyzeArgs args = parseAnalyzeCommand(command, pieces, pla, parseFailed);
GTPEngine::AnalyzeArgs args = parseAnalyzeCommand(command, pieces, engine, parseFailed);
if(parseFailed) {
responseIsError = true;
response = "Could not parse genmove_analyze arguments or arguments out of range: '" + Global::concat(pieces," ") + "'";
Expand Down Expand Up @@ -2337,10 +2363,10 @@ int MainCmds::gtp(int argc, const char* const* argv) {
}
}

else if(command == "analyze" || command == "lz-analyze" || command == "kata-analyze") {
else if(command == "analyze" || command == "lz-analyze" || command == "kata-analyze" || command == "kata-problem_analyze") {
Player pla = engine->bot->getRootPla();
bool parseFailed = false;
GTPEngine::AnalyzeArgs args = parseAnalyzeCommand(command, pieces, pla, parseFailed);
GTPEngine::AnalyzeArgs args = parseAnalyzeCommand(command, pieces, engine, parseFailed);

if(parseFailed) {
responseIsError = true;
Expand Down
14 changes: 14 additions & 0 deletions cpp/game/boardhistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,20 @@ bool BoardHistory::isLegal(const Board& board, Loc moveLoc, Player movePla) cons
return true;
}

bool BoardHistory::isLegalAllowSuperKo(const Board& board, Loc moveLoc, Player movePla) const {
//Ko-moves in the encore that are recapture blocked are interpreted as pass-for-ko, so they are legal
if(encorePhase > 0 && moveLoc >= 0 && moveLoc < Board::MAX_ARR_SIZE && moveLoc != Board::PASS_LOC) {
Loc koCaptureLoc = board.getKoCaptureLoc(moveLoc,movePla);
if(koCaptureLoc != Board::NULL_LOC && koRecapBlocked[koCaptureLoc] && board.colors[koCaptureLoc] == getOpp(movePla))
return true;
}

if(!board.isLegal(moveLoc,movePla,rules.multiStoneSuicideLegal))
return false;

return true;
}

bool BoardHistory::isPassForKo(const Board& board, Loc moveLoc, Player movePla) const {
if(encorePhase > 0 && moveLoc >= 0 && moveLoc < Board::MAX_ARR_SIZE && moveLoc != Board::PASS_LOC) {
Loc koCaptureLoc = board.getKoCaptureLoc(moveLoc,movePla);
Expand Down
1 change: 1 addition & 0 deletions cpp/game/boardhistory.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ struct BoardHistory {

//Check if a move on the board is legal, taking into account the full game state and superko
bool isLegal(const Board& board, Loc moveLoc, Player movePla) const;
bool isLegalAllowSuperKo(const Board& board, Loc moveLoc, Player movePla) const;
//Check if passing right now would end the current phase of play, or the entire game
bool passWouldEndPhase(const Board& board, Player movePla) const;
bool passWouldEndGame(const Board& board, Player movePla) const;
Expand Down
16 changes: 16 additions & 0 deletions cpp/search/asyncbot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ void AsyncBot::setAlwaysIncludeOwnerMap(bool b) {
stopAndWait();
search->setAlwaysIncludeOwnerMap(b);
}

void AsyncBot::setProblemAnalyze(bool b) {
stopAndWait();
search->setProblemAnalyze(b);
}

void AsyncBot::setProblemAnalyzeTopLeftCorner(Loc b) {
stopAndWait();
search->setProblemAnalyzeTopLeftCorner(b);
}

void AsyncBot::setProblemAnalyzeBottomRightCorner(Loc b) {
stopAndWait();
search->setProblemAnalyzeBottomRightCorner(b);
}

void AsyncBot::setParams(SearchParams params) {
stopAndWait();
search->setParams(params);
Expand Down
3 changes: 3 additions & 0 deletions cpp/search/asyncbot.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class AsyncBot {
void setRootPassLegal(bool b);
void setRootHintLoc(Loc loc);
void setAlwaysIncludeOwnerMap(bool b);
void setProblemAnalyze(bool b);
void setProblemAnalyzeTopLeftCorner(Loc b);
void setProblemAnalyzeBottomRightCorner(Loc b);
void setParams(SearchParams params);
void setPlayerIfNew(Player movePla);
void clearSearch();
Expand Down
101 changes: 86 additions & 15 deletions cpp/search/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ Search::Search(SearchParams params, NNEvaluator* nnEval, const string& rSeed)
rootSafeArea(NULL),
recentScoreCenter(0.0),
alwaysIncludeOwnerMap(false),
isProblemAnalyze(false),
problemAnalyzeTopLeftCorner(Board::NULL_LOC),
problemAnalyzeBottomRightCorner(Board::NULL_LOC),
searchParams(params),numSearchesBegun(0),searchNodeAge(0),
plaThatSearchIsFor(C_EMPTY),plaThatSearchIsForLastSearch(C_EMPTY),
lastSearchNumPlayouts(0),
Expand Down Expand Up @@ -243,6 +246,24 @@ void Search::setAlwaysIncludeOwnerMap(bool b) {
clearSearch();
alwaysIncludeOwnerMap = b;
}
void Search::setProblemAnalyze(bool b) {
if(!isProblemAnalyze && b)
clearSearch();
isProblemAnalyze = b;
}

void Search::setProblemAnalyzeTopLeftCorner(Loc b) {
if (problemAnalyzeTopLeftCorner != b)
clearSearch();
problemAnalyzeTopLeftCorner = b;
}

void Search::setProblemAnalyzeBottomRightCorner(Loc b) {
if (problemAnalyzeBottomRightCorner != b)
clearSearch();
problemAnalyzeBottomRightCorner = b;
}


void Search::setParams(SearchParams params) {
clearSearch();
Expand Down Expand Up @@ -957,6 +978,33 @@ void Search::maybeAddPolicyNoiseAndTempAlreadyLocked(SearchThread& thread, Searc
}
}

bool Search::isInProblemArea(Loc moveLoc) const {
assert(moveLoc == Board::PASS_LOC || rootBoard.isOnBoard(moveLoc));
if (problemAnalyzeTopLeftCorner == Board::NULL_LOC || problemAnalyzeBottomRightCorner == Board::NULL_LOC) {
// not limit
return true;
}
int x = Location::getX(moveLoc, rootBoard.x_size);
int y = Location::getY(moveLoc, rootBoard.x_size);
int x1 = Location::getX(problemAnalyzeTopLeftCorner, rootBoard.x_size);
int x2 = Location::getX(problemAnalyzeBottomRightCorner, rootBoard.x_size);
int y1 = Location::getY(problemAnalyzeTopLeftCorner, rootBoard.x_size);
int y2 = Location::getY(problemAnalyzeBottomRightCorner, rootBoard.x_size);
if (x1 > x2) {
// swap
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y1 > y2) {
// swap
int tmp = y1;
y1 = y2;
y2 = tmp;
}
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
}

bool Search::isAllowedRootMove(Loc moveLoc) const {
assert(moveLoc == Board::PASS_LOC || rootBoard.isOnBoard(moveLoc));

Expand Down Expand Up @@ -1428,6 +1476,9 @@ void Search::selectBestChildToDescend(
if(moveLoc == Board::NULL_LOC)
continue;

if(isProblemAnalyze && !isInProblemArea(moveLoc))
continue;

//Special logic for the root
if(isRoot) {
assert(thread.board.pos_hash == rootBoard.pos_hash);
Expand Down Expand Up @@ -1624,7 +1675,7 @@ void Search::recomputeNodeStats(SearchNode& node, SearchThread& thread, int numV

void Search::runSinglePlayout(SearchThread& thread) {
bool posesWithChildBuf[NNPos::MAX_NN_POLICY_SIZE];
playoutDescend(thread,*rootNode,posesWithChildBuf,true,0);
playoutDescend(thread,*rootNode,posesWithChildBuf,true,0, 0);

//Restore thread state back to the root state
thread.pla = rootPla;
Expand Down Expand Up @@ -1743,7 +1794,8 @@ void Search::initNodeNNOutput(
void Search::playoutDescend(
SearchThread& thread, SearchNode& node,
bool posesWithChildBuf[NNPos::MAX_NN_POLICY_SIZE],
bool isRoot, int32_t virtualLossesToSubtract
bool isRoot, int32_t virtualLossesToSubtract,
int32_t depth
) {
//Hit terminal node, finish
//In the case where we're forcing the search to make another move at the root, don't terminate, actually run search for a move more.
Expand Down Expand Up @@ -1798,21 +1850,38 @@ void Search::playoutDescend(
//The absurdly rare case that the move chosen is not legal
//(this should only happen either on a bug or where the nnHash doesn't have full legality information or when there's an actual hash collision).
//Regenerate the neural net call and continue
if(!thread.history.isLegal(thread.board,bestChildMoveLoc,thread.pla)) {
bool isReInit = true;
initNodeNNOutput(thread,node,isRoot,true,0,isReInit);

if(thread.logStream != NULL)
(*thread.logStream) << "WARNING: Chosen move not legal so regenerated nn output, nnhash=" << node.nnOutput->nnHash << endl;

//As isReInit is true, we don't return, just keep going, since we didn't count this as a true visit in the node stats
selectBestChildToDescend(thread,node,bestChildIdx,bestChildMoveLoc,posesWithChildBuf,isRoot);
//We should absolutely be legal this time
assert(thread.history.isLegal(thread.board,bestChildMoveLoc,thread.pla));
if (isProblemAnalyze) {
if(!thread.history.isLegalAllowSuperKo(thread.board,bestChildMoveLoc,thread.pla)) {
bool isReInit = true;
initNodeNNOutput(thread,node,isRoot,true,0,isReInit);

if(thread.logStream != NULL)
(*thread.logStream) << "WARNING: Chosen move not legal so regenerated nn output, nnhash=" << node.nnOutput->nnHash << endl;

//As isReInit is true, we don't return, just keep going, since we didn't count this as a true visit in the node stats
selectBestChildToDescend(thread,node,bestChildIdx,bestChildMoveLoc,posesWithChildBuf,isRoot);
// We should absolutely be legal this time
// assert(thread.history.isLegalAllowSuperKo(thread.board,bestChildMoveLoc,thread.pla));
}
} else {
if(!thread.history.isLegal(thread.board,bestChildMoveLoc,thread.pla)) {
bool isReInit = true;
initNodeNNOutput(thread,node,isRoot,true,0,isReInit);

if(thread.logStream != NULL)
(*thread.logStream) << "WARNING: Chosen move not legal so regenerated nn output, nnhash=" << node.nnOutput->nnHash << endl;

//As isReInit is true, we don't return, just keep going, since we didn't count this as a true visit in the node stats
selectBestChildToDescend(thread,node,bestChildIdx,bestChildMoveLoc,posesWithChildBuf,isRoot);
//We should absolutely be legal this time
assert(thread.history.isLegal(thread.board,bestChildMoveLoc,thread.pla));
}
}


if(bestChildIdx < -1) {
lock.unlock();
assert(false);
throw StringError("Search error: No move with sane selection value - can't even pass?");
}

Expand Down Expand Up @@ -1855,8 +1924,10 @@ void Search::playoutDescend(
thread.pla = getOpp(thread.pla);

//Recurse!
playoutDescend(thread,*child,posesWithChildBuf,false,searchParams.numVirtualLossesPerThread);

if (!isProblemAnalyze || depth < 50) {
playoutDescend(thread,*child,posesWithChildBuf,false,searchParams.numVirtualLossesPerThread, depth + 1);
}

//Update this node stats
updateStatsAfterPlayout(node,thread,virtualLossesToSubtract,isRoot);
}
13 changes: 11 additions & 2 deletions cpp/search/search.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ struct Search {

bool alwaysIncludeOwnerMap;

bool isProblemAnalyze;
Loc problemAnalyzeTopLeftCorner;
Loc problemAnalyzeBottomRightCorner;

SearchParams searchParams;
int64_t numSearchesBegun;
uint32_t searchNodeAge;
Expand Down Expand Up @@ -201,6 +205,9 @@ struct Search {
void setKomiIfNew(float newKomi); //Does not clear history, does clear search unless komi is equal.
void setRootPassLegal(bool b);
void setRootHintLoc(Loc hintLoc);
void setProblemAnalyze(bool b);
void setProblemAnalyzeTopLeftCorner(Loc b);
void setProblemAnalyzeBottomRightCorner(Loc b);
void setAlwaysIncludeOwnerMap(bool b);
void setParams(SearchParams params);
void setParamsNoClearing(SearchParams params); //Does not clear search
Expand Down Expand Up @@ -320,7 +327,8 @@ struct Search {
int getPos(Loc moveLoc) const;

bool isAllowedRootMove(Loc moveLoc) const;

bool isInProblemArea(Loc moveLoc) const;

void computeRootValues();

double getScoreUtility(double scoreMeanSum, double scoreMeanSqSum, double weightSum) const;
Expand Down Expand Up @@ -399,7 +407,8 @@ struct Search {
void playoutDescend(
SearchThread& thread, SearchNode& node,
bool posesWithChildBuf[NNPos::MAX_NN_POLICY_SIZE],
bool isRoot, int32_t virtualLossesToSubtract
bool isRoot, int32_t virtualLossesToSubtract,
int32_t depth
);

bool shouldSuppressPassAlreadyLocked(const SearchNode* n) const;
Expand Down
10 changes: 9 additions & 1 deletion docs/GTP_Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ In addition to a basic set of [GTP commands](https://www.lysator.liu.se/~gunnar/
* Like `lz-analyze`, will immediately begin printing a partial GTP response, with a new line every `interval` centiseconds.
* Unlike `lz-analyze`, will teriminate on its own after the normal amount of time that a `genmove` would take and will NOT terminate prematurely or asynchronously upon recipt of a newline or an additional GTP command.
* The final move made will be reported as a single line `play <vertex or "pass" or "resign">`, followed by the usual double-newline that signals a complete GTP response.
* `kata-genmove_analyze [player (optional)] [interval (optional)] KEYVALUEPAIR KEYVALUEPAIR`
* `kata-problem_analyze [player (optional)] [interval (optional)] KEYVALUEPAIR KEYVALUEPAIR`
* this extented GTP command is used for solving life-dead problems.
* Same as `kata-analyze` except with the options and fields :
* Additional possible key-value pairs:
* `topleft M19` - Sets the problem valid area - the top left corner
* `bottomright T14` - Sets the problem valid area - the bottom right corner
* if `topoleft` or `bottomright` is not set, use the full board.

* `kata-genmove_analyze [player (optional)] [interval (optional)] KEYVALUEPAIR KEYVALUEPAIR`
* Same as `lz-genmove_analyze` except with the options and fields of `kata-analyze` rather than `lz-analyze`
* `analyze, genmove_analyze`
* Same as `kata-analyze` and `kata-genmove_analyze`, but intended specifically for the Sabaki GUI app in that all floating point values are always formatted with a decimal point, even when a value happens to be an integer. May also have slightly less compact output in other ways (e.g. extra trailing zeros on some decimals).
Expand Down