diff --git a/.gitignore b/.gitignore index 29b52cbf6..1cb085c06 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,8 @@ __pycache__ *.cbp tmp*.txt - +test/ +.vscode/ cpp/write cpp/runtests cpp/example diff --git a/cpp/command/gtp.cpp b/cpp/command/gtp.cpp index 996c4bf1a..34f4c2e90 100644 --- a/cpp/command/gtp.cpp +++ b/cpp/command/gtp.cpp @@ -69,7 +69,7 @@ static const vector knownCommands = { // "analyze", "lz-analyze", "kata-analyze", - + "kata-problem_analyze", //Display raw neural net evaluations "kata-raw-nn", @@ -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 getAnalyzeCallback(Player pla, AnalyzeArgs args) { @@ -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); } @@ -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& pieces, Player& pla, bool& parseFailed) { +static GTPEngine::AnalyzeArgs parseAnalyzeCommand(const string& command, const vector& 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: @@ -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; @@ -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; } @@ -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," ") + "'"; @@ -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; diff --git a/cpp/game/boardhistory.cpp b/cpp/game/boardhistory.cpp index 0b9271c73..54993a01d 100644 --- a/cpp/game/boardhistory.cpp +++ b/cpp/game/boardhistory.cpp @@ -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); diff --git a/cpp/game/boardhistory.h b/cpp/game/boardhistory.h index 426323aa5..b162dca39 100644 --- a/cpp/game/boardhistory.h +++ b/cpp/game/boardhistory.h @@ -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; diff --git a/cpp/search/asyncbot.cpp b/cpp/search/asyncbot.cpp index f90481b67..1660424a1 100644 --- a/cpp/search/asyncbot.cpp +++ b/cpp/search/asyncbot.cpp @@ -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); diff --git a/cpp/search/asyncbot.h b/cpp/search/asyncbot.h index fe7235a13..e5c9be76f 100644 --- a/cpp/search/asyncbot.h +++ b/cpp/search/asyncbot.h @@ -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(); diff --git a/cpp/search/search.cpp b/cpp/search/search.cpp index 5da8e416b..cc488a195 100644 --- a/cpp/search/search.cpp +++ b/cpp/search/search.cpp @@ -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), @@ -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(); @@ -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)); @@ -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); @@ -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; @@ -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. @@ -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?"); } @@ -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); } diff --git a/cpp/search/search.h b/cpp/search/search.h index 7c7adf4c6..0c1123d02 100644 --- a/cpp/search/search.h +++ b/cpp/search/search.h @@ -147,6 +147,10 @@ struct Search { bool alwaysIncludeOwnerMap; + bool isProblemAnalyze; + Loc problemAnalyzeTopLeftCorner; + Loc problemAnalyzeBottomRightCorner; + SearchParams searchParams; int64_t numSearchesBegun; uint32_t searchNodeAge; @@ -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 @@ -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; @@ -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; diff --git a/docs/GTP_Extensions.md b/docs/GTP_Extensions.md index 12b1e6ab7..512094e3d 100644 --- a/docs/GTP_Extensions.md +++ b/docs/GTP_Extensions.md @@ -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 `, 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).