00001 //---------------------------------------------------------------------------- 00002 /** @file MoHexPlayer.cpp 00003 */ 00004 //---------------------------------------------------------------------------- 00005 00006 #include "SgSystem.h" 00007 00008 #include "SgTimer.h" 00009 #include "SgUctTreeUtil.h" 00010 00011 #include "BitsetIterator.hpp" 00012 #include "BoardUtils.hpp" 00013 #include "VCSet.hpp" 00014 #include "HexUctUtil.hpp" 00015 #include "HexUctKnowledge.hpp" 00016 #include "HexUctSearch.hpp" 00017 #include "HexUctPolicy.hpp" 00018 #include "MoHexPlayer.hpp" 00019 #include "EndgameUtils.hpp" 00020 #include "Resistance.hpp" 00021 #include "SequenceHash.hpp" 00022 #include "Time.hpp" 00023 #include "VCUtils.hpp" 00024 00025 using namespace benzene; 00026 00027 //---------------------------------------------------------------------------- 00028 00029 namespace { 00030 00031 /** Returns true if one is a prefix of the other. */ 00032 bool IsPrefixOf(const MoveSequence& a, const MoveSequence& b) 00033 { 00034 for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) 00035 { 00036 if (a[i] != b[i]) 00037 return false; 00038 } 00039 return true; 00040 } 00041 00042 00043 void SortConsiderSet(const bitset_t& consider, const Resistance& resist, 00044 std::vector<HexPoint>& moves) 00045 { 00046 std::vector<std::pair<HexEval, HexPoint> > mvsc; 00047 for (BitsetIterator it(consider); it; ++it) 00048 mvsc.push_back(std::make_pair(-resist.Score(*it), *it)); 00049 stable_sort(mvsc.begin(), mvsc.end()); 00050 moves.clear(); 00051 for (std::size_t i = 0; i < mvsc.size(); ++i) 00052 moves.push_back(mvsc[i].second); 00053 } 00054 00055 } 00056 00057 //---------------------------------------------------------------------------- 00058 00059 MoHexPlayer::MoHexPlayer() 00060 : BenzenePlayer(), 00061 m_shared_policy(), 00062 m_search(new HexThreadStateFactory(&m_shared_policy), 00063 HexUctUtil::ComputeMaxNumMoves()), 00064 m_backup_ice_info(true), 00065 m_max_games(99999999), 00066 m_max_time(10), 00067 m_useTimeManagement(false), 00068 m_reuse_subtree(false), 00069 m_ponder(false), 00070 m_performPreSearch(true) 00071 { 00072 } 00073 00074 MoHexPlayer::~MoHexPlayer() 00075 { 00076 } 00077 00078 void MoHexPlayer::CopySettingsFrom(const MoHexPlayer& other) 00079 { 00080 SetBackupIceInfo(other.BackupIceInfo()); 00081 Search().SetLockFree(other.Search().LockFree()); 00082 Search().SetLiveGfx(other.Search().LiveGfx()); 00083 Search().SetRave(other.Search().Rave()); 00084 Search().SetBiasTermConstant(other.Search().BiasTermConstant()); 00085 Search().SetExpandThreshold(other.Search().ExpandThreshold()); 00086 Search().SetLiveGfxInterval(other.Search().LiveGfxInterval()); 00087 SetMaxGames(other.MaxGames()); 00088 SetMaxTime(other.MaxTime()); 00089 SetPerformPreSearch(other.PerformPreSearch()); 00090 SetUseTimeManagement(other.UseTimeManagement()); 00091 Search().SetMaxNodes(other.Search().MaxNodes()); 00092 Search().SetNumberThreads(other.Search().NumberThreads()); 00093 Search().SetPlayoutUpdateRadius(other.Search().PlayoutUpdateRadius()); 00094 Search().SetRandomizeRaveFrequency 00095 (other.Search().RandomizeRaveFrequency()); 00096 Search().SetRaveWeightFinal(other.Search().RaveWeightFinal()); 00097 Search().SetRaveWeightInitial(other.Search().RaveWeightInitial()); 00098 Search().SetWeightRaveUpdates(other.Search().WeightRaveUpdates()); 00099 Search().SetTreeUpdateRadius(other.Search().TreeUpdateRadius()); 00100 Search().SetKnowledgeThreshold(other.Search().KnowledgeThreshold()); 00101 } 00102 00103 //---------------------------------------------------------------------------- 00104 00105 HexPoint MoHexPlayer::Search(const HexState& state, const Game& game, 00106 HexBoard& brd, const bitset_t& given_to_consider, 00107 double maxTime, double& score) 00108 { 00109 HexAssert(!brd.GetGroups().IsGameOver()); 00110 HexColor color = state.ToPlay(); 00111 00112 SgTimer totalElapsed; 00113 PrintParameters(color, maxTime); 00114 00115 // Do presearch and abort if win found. Allow it to take 20% of 00116 // total time. 00117 SgTimer timer; 00118 bitset_t consider = given_to_consider; 00119 PointSequence winningSequence; 00120 if (m_performPreSearch && PerformPreSearch(brd, color, consider, 00121 maxTime * 0.2, winningSequence)) 00122 { 00123 LogInfo() << "Winning sequence found before UCT search!\n" 00124 << "Sequence: " << winningSequence[0] << '\n'; 00125 score = IMMEDIATE_WIN; 00126 return winningSequence[0]; 00127 } 00128 timer.Stop(); 00129 LogInfo() << "Time for PreSearch: " 00130 << Time::Formatted(timer.GetTime()) << '\n'; 00131 00132 maxTime -= timer.GetTime(); 00133 maxTime = std::max(1.0, maxTime); 00134 00135 // Create the initial state data 00136 HexUctSharedData data; 00137 data.board_width = brd.Width(); 00138 data.board_height = brd.Height(); 00139 data.root_to_play = color; 00140 data.game_sequence = game.History(); 00141 data.root_last_move_played = LastMoveFromHistory(game.History()); 00142 data.root_stones = HexUctStoneData(brd.GetPosition()); 00143 data.root_consider = consider; 00144 00145 // Reuse the old subtree 00146 SgUctTree* initTree = 0; 00147 if (m_reuse_subtree) 00148 { 00149 HexUctSharedData oldData = m_search.SharedData(); 00150 initTree = TryReuseSubtree(oldData, data); 00151 if (!initTree) 00152 LogInfo() << "No subtree to reuse.\n"; 00153 } 00154 m_search.SetSharedData(data); 00155 00156 brd.GetPatternState().ClearPatternCheckStats(); 00157 int old_radius = brd.GetPatternState().UpdateRadius(); 00158 brd.GetPatternState().SetUpdateRadius(m_search.TreeUpdateRadius()); 00159 00160 // Do the search 00161 std::vector<SgMove> sequence; 00162 std::vector<SgMove> rootFilter; 00163 m_search.SetBoard(brd); 00164 score = m_search.Search(m_max_games, maxTime, sequence, 00165 rootFilter, initTree, 0); 00166 00167 brd.GetPatternState().SetUpdateRadius(old_radius); 00168 00169 // Output stats 00170 std::ostringstream os; 00171 os << '\n'; 00172 #if COLLECT_PATTERN_STATISTICS 00173 os << m_shared_policy.DumpStatistics() << '\n'; 00174 os << brd.DumpPatternCheckStats() << '\n'; 00175 #endif 00176 os << "Elapsed Time " << Time::Formatted(totalElapsed.GetTime()) << '\n'; 00177 m_search.WriteStatistics(os); 00178 os << "Score " << std::setprecision(2) << score << "\n" 00179 << "Sequence "; 00180 for (std::size_t i = 0; i < sequence.size(); i++) 00181 os << " " << HexUctUtil::MoveString(sequence[i]); 00182 os << '\n'; 00183 00184 LogInfo() << os.str() << '\n'; 00185 00186 #if 0 00187 if (m_save_games) 00188 { 00189 std::string filename = "uct-games.sgf"; 00190 uct.SaveGames(filename); 00191 LogInfo() << "Games saved in '" << filename << "'.\n"; 00192 } 00193 #endif 00194 00195 // Return move recommended by HexUctSearch 00196 if (sequence.size() > 0) 00197 return static_cast<HexPoint>(sequence[0]); 00198 00199 // It is possible that HexUctSearch did only 1 simulation (probably 00200 // because it ran out of time to do more); in this case, the move 00201 // sequence is empty and so we give a warning and return a random 00202 // move. 00203 LogWarning() << "**** HexUctSearch returned empty sequence!\n" 00204 << "**** Returning random move!\n"; 00205 return BoardUtils::RandomEmptyCell(brd.GetPosition()); 00206 } 00207 00208 /** Returns INVALID_POINT if history is empty, otherwise last move 00209 played to the board, ie, skips swap move. */ 00210 HexPoint MoHexPlayer::LastMoveFromHistory(const MoveSequence& history) 00211 { 00212 HexPoint lastMove = INVALID_POINT; 00213 if (!history.empty()) 00214 { 00215 lastMove = history.back().point(); 00216 if (lastMove == SWAP_PIECES) 00217 { 00218 HexAssert(history.size() == 2); 00219 lastMove = history.front().point(); 00220 } 00221 } 00222 return lastMove; 00223 } 00224 00225 /** Does a 1-ply search. 00226 00227 For each move in the consider set, if the move is a win, returns 00228 true and the move. If the move is a loss, prune it out of the 00229 consider set if there are non-losing moves in the consider set. 00230 If all moves are losing, perform no pruning, search will resist. 00231 00232 Returns true if there is a win, false otherwise. 00233 00234 @todo Is it true that MoHex will resist in the strongest way 00235 possible? 00236 */ 00237 bool MoHexPlayer::PerformPreSearch(HexBoard& brd, HexColor color, 00238 bitset_t& consider, float maxTime, 00239 PointSequence& winningSequence) 00240 { 00241 00242 bitset_t losing; 00243 HexColor other = !color; 00244 PointSequence seq; 00245 bool foundWin = false; 00246 00247 SgTimer elapsed; 00248 Resistance resist; 00249 resist.Evaluate(brd); 00250 std::vector<HexPoint> moves; 00251 SortConsiderSet(consider, resist, moves); 00252 for (std::size_t i = 0; i < moves.size() && !foundWin; ++i) 00253 { 00254 if (elapsed.GetTime() > maxTime) 00255 { 00256 LogInfo() << "PreSearch: max time reached " 00257 << '(' << i << '/' << moves.size() << ").\n"; 00258 break; 00259 } 00260 brd.PlayMove(color, moves[i]); 00261 seq.push_back(moves[i]); 00262 if (EndgameUtils::IsLostGame(brd, other)) // Check for winning move 00263 { 00264 winningSequence = seq; 00265 foundWin = true; 00266 } 00267 else if (EndgameUtils::IsWonGame(brd, other)) 00268 losing.set(moves[i]); 00269 seq.pop_back(); 00270 brd.UndoMove(); 00271 } 00272 00273 // Abort if we found a one-move win 00274 if (foundWin) 00275 return true; 00276 00277 // Backing up cannot cause this to happen, right? 00278 HexAssert(!EndgameUtils::IsDeterminedState(brd, color)); 00279 00280 // Use the backed-up ice info to shrink the moves to consider 00281 if (m_backup_ice_info) 00282 { 00283 bitset_t new_consider 00284 = EndgameUtils::MovesToConsider(brd, color) & consider; 00285 00286 if (new_consider.count() < consider.count()) 00287 { 00288 consider = new_consider; 00289 LogFine() << "$$$$$$ new moves to consider $$$$$$" 00290 << brd.Write(consider) << '\n'; 00291 } 00292 } 00293 00294 // Subtract any losing moves from the set we consider, unless all of them 00295 // are losing (in which case UCT search will find which one resists the 00296 // loss well). 00297 if (losing.any()) 00298 { 00299 if (BitsetUtil::IsSubsetOf(consider, losing)) 00300 LogInfo() << "************************************\n" 00301 << " All UCT root children are losing!!\n" 00302 << "************************************\n"; 00303 else 00304 { 00305 LogFine() << "Removed losing moves: " << brd.Write(losing) << '\n'; 00306 consider = consider - losing; 00307 } 00308 } 00309 HexAssert(consider.any()); 00310 LogInfo()<< "Moves to consider:\n" << brd.Write(consider) << '\n'; 00311 return false; 00312 } 00313 00314 void MoHexPlayer::PrintParameters(HexColor color, double timeForMove) 00315 { 00316 LogInfo() << "--- MoHexPlayer::Search() ---\n" 00317 << "Color: " << color << '\n' 00318 << "MaxGames: " << m_max_games << '\n' 00319 << "NumberThreads: " << m_search.NumberThreads() << '\n' 00320 << "MaxNodes: " << m_search.MaxNodes() 00321 << " (" << sizeof(SgUctNode)*m_search.MaxNodes() << " bytes)\n" 00322 << "TimeForMove: " << timeForMove << '\n'; 00323 } 00324 00325 /** Extracts relevant portion of old tree for use in upcoming search. 00326 Returns valid pointer to new tree on success, 0 on failure. */ 00327 SgUctTree* MoHexPlayer::TryReuseSubtree(const HexUctSharedData& oldData, 00328 HexUctSharedData& newData) 00329 { 00330 // Must have knowledge on to reuse subtrees, since root has fillin 00331 // knowledge which affects the tree below. 00332 if (m_search.KnowledgeThreshold().empty()) 00333 { 00334 LogInfo() << "ReuseSubtree: knowledge is off.\n"; 00335 return 0; 00336 } 00337 00338 // Board size must be the same. This also catches the case where 00339 // no previous search has been performed. 00340 if (oldData.board_width != newData.board_width || 00341 oldData.board_height != newData.board_height) 00342 return 0; 00343 00344 const MoveSequence& oldSequence = oldData.game_sequence; 00345 const MoveSequence& newSequence = newData.game_sequence; 00346 00347 LogInfo() << "Old: " << oldSequence << '\n'; 00348 LogInfo() << "New: "<< newSequence << '\n'; 00349 00350 if (oldSequence.size() > newSequence.size()) 00351 { 00352 LogInfo() << "ReuseSubtree: Backtracked to an earlier state.\n"; 00353 return 0; 00354 } 00355 if (!IsPrefixOf(oldSequence, newSequence)) 00356 { 00357 LogInfo() << "ReuseSubtree: Not a continuation.\n"; 00358 return 0; 00359 } 00360 00361 bool samePosition = (oldSequence == newSequence 00362 && oldData.root_to_play == newData.root_to_play 00363 && oldData.root_consider == newData.root_consider 00364 && oldData.root_stones == newData.root_stones); 00365 00366 if (samePosition) 00367 LogInfo() << "ReuseSubtree: in same position as last time!\n"; 00368 00369 // If no old knowledge for the new root in the old tree, then we 00370 // cannot reuse the tree (since the root is given its knowledge 00371 // and using this knowledge would require pruning the trees under 00372 // the root's children). 00373 if (!samePosition) 00374 { 00375 HexUctStoneData oldStateData; 00376 hash_t hash = SequenceHash::Hash(newSequence); 00377 if (!oldData.stones.get(hash, oldStateData)) 00378 { 00379 LogInfo() << "ReuseSubtree: No knowledge for state in old tree.\n"; 00380 return 0; 00381 } 00382 00383 // Check that the old knowledge is equal to the new knowledge 00384 // in the would-be root node. 00385 if (!(oldStateData == newData.root_stones)) 00386 { 00387 StoneBoard brd(11); 00388 brd.SetColor(BLACK, oldStateData.black); 00389 brd.SetColor(WHITE, oldStateData.white); 00390 brd.SetPlayed(oldStateData.played); 00391 LogWarning() << "FILLIN DOES NOT MATCH\n"; 00392 LogWarning() << brd << '\n'; 00393 brd.StartNewGame(); 00394 brd.SetColor(BLACK, newData.root_stones.black); 00395 brd.SetColor(WHITE, newData.root_stones.white); 00396 brd.SetPlayed(newData.root_stones.played); 00397 LogWarning() << brd << '\n'; 00398 } 00399 HexAssert(oldStateData == newData.root_stones); 00400 } 00401 00402 // Ensure alternating colors and extract suffix 00403 MoveSequence suffix; 00404 std::vector<SgMove> sequence; 00405 for (std::size_t i = oldSequence.size(); i < newSequence.size(); ++i) 00406 { 00407 if (i && newSequence[i-1].color() == newSequence[i].color()) 00408 { 00409 LogInfo() << "ReuseSubtree: Colors do not alternate.\n"; 00410 return 0; 00411 } 00412 suffix.push_back(newSequence[i]); 00413 sequence.push_back(newSequence[i].point()); 00414 } 00415 LogInfo() << "MovesPlayed: " << suffix << '\n'; 00416 00417 // Extract the tree 00418 SgUctTree& tree = m_search.GetTempTree(); 00419 SgUctTreeUtil::ExtractSubtree(m_search.Tree(), tree, sequence, true, 10.0); 00420 00421 std::size_t newTreeNodes = tree.NuNodes(); 00422 std::size_t oldTreeNodes = m_search.Tree().NuNodes(); 00423 00424 if (oldTreeNodes > 1 && newTreeNodes > 1) 00425 { 00426 // Fix root's children to be those in the consider set 00427 std::vector<SgMove> moves; 00428 for (BitsetIterator it(newData.root_consider); it; ++it) 00429 moves.push_back(static_cast<SgMove>(*it)); 00430 tree.SetChildren(0, tree.Root(), moves); 00431 00432 float reuse = static_cast<float>(newTreeNodes) / oldTreeNodes; 00433 int reusePercent = static_cast<int>(100 * reuse); 00434 LogInfo() << "MoHexPlayer: Reusing " << newTreeNodes 00435 << " nodes (" << reusePercent << "%)\n"; 00436 00437 MoveSequence moveSequence = newSequence; 00438 CopyKnowledgeData(tree, tree.Root(), newData.root_to_play, 00439 moveSequence, oldData, newData); 00440 float kReuse = static_cast<float>(newData.stones.count()) 00441 / oldData.stones.count(); 00442 int kReusePercent = static_cast<int>(100 * kReuse); 00443 LogInfo() << "MoHexPlayer: Reusing " 00444 << newData.stones.count() << " knowledge states (" 00445 << kReusePercent << "%)\n"; 00446 return &tree; 00447 } 00448 return 0; 00449 } 00450 00451 void MoHexPlayer::CopyKnowledgeData(const SgUctTree& tree, 00452 const SgUctNode& node, 00453 HexColor color, MoveSequence& sequence, 00454 const HexUctSharedData& oldData, 00455 HexUctSharedData& newData) const 00456 { 00457 // This check will fail in the root if we are reusing the 00458 // entire tree, so only do it when not in the root. 00459 if (sequence != oldData.game_sequence) 00460 { 00461 hash_t hash = SequenceHash::Hash(sequence); 00462 HexUctStoneData stones; 00463 if (!oldData.stones.get(hash, stones)) 00464 return; 00465 newData.stones.put(hash, stones); 00466 } 00467 if (!node.HasChildren()) 00468 return; 00469 for (SgUctChildIterator it(tree, node); it; ++it) 00470 { 00471 sequence.push_back(Move(color, static_cast<HexPoint>((*it).Move()))); 00472 CopyKnowledgeData(tree, *it, !color, sequence, oldData, newData); 00473 sequence.pop_back(); 00474 } 00475 } 00476 00477 //----------------------------------------------------------------------------