有了棋串的資料結構後,落子就變得很高效了,接下來要產生隨機棋步。
以9x9棋盤為例,直接產生0~80的隨機數進行類比會很低效,因為它們並不都是合法落子點——有3類落子點是非法的:1. 非空點 2. 劫爭點 3. 自殺點。
自然想在類比過程中增量維護這3類點集,前兩類很容易維護,痛點在於自殺點的增量維護。
著手寫自殺點的維護後,才發現問題出乎意料的複雜。我先寫了個IsSuiside函數,通過試下作為最終的裁決,而這又涉及到對象的拷貝,太過昂貴。於是想盡量少用,通過位元運算來判定多數情況。然後隨著測試的進行,陸續發現多處的位元運算判定不可靠,不得不用IsSuiside函數替代……
最後的效率可想而知,9 x 9棋盤類比1w局,用時約40秒——這是不可授受的。
回過頭來思考,越發覺得這件事並不必要做。多數情況下,自殺點本身並無價值(除了打劫時也許可作劫材),即使允許自殺,在UCT搜尋過程中,自殺點自然會冷下來——用過多的資源判定自殺點划不來。
於是修改程式,讓規則允許自殺,不過為了儘早結束棋局,不允許自填眼位,也不允許填對方眼位而不提子。增量維護眼位集合要簡單得多。
測試下來要快很多,9 x 9棋盤,2.3秒類比1萬局,CPU是2.4G core 2,編譯器是clang,開O3最佳化級。
Simulate函數:
template <BoardLen BOARD_LEN>PointIndexMCSimulator<BOARD_LEN>::Simulate(const BoardInGm<BOARD_LEN> &input_board) const{ BoardInGm<BOARD_LEN> bingm; bingm.Copy(input_board); do { PlayerColor last_player = bingm.LastPlayer(); PlayerColor cur_player = OppstColor(last_player); const auto &playable = bingm.PlayableIndexes(cur_player); std::bitset<BLSq<BOARD_LEN>()> noko_plbl(playable); PointIndex ko = bingm.KoIndex(); if (ko != BoardInGm<BOARD_LEN>::NONE) { std::bitset<BLSq<BOARD_LEN>()> kobits; kobits.set(); kobits.reset(ko); noko_plbl &= kobits; } PointIndex play_c = noko_plbl.count(); if (play_c > 0) { PointIndex rand = this->Rand(play_c - 1); PointIndex cur_indx = GetXst1<BLSq<BOARD_LEN>()>(noko_plbl, rand); bingm.PlayMove(Move(cur_player, cur_indx)); } else { bingm.Pass(cur_player); } } while(bingm.PlayableIndexes(BLACK_PLAYER).count() > 0 || bingm.PlayableIndexes(WHITE_PLAYER).count() > 0); return bingm.BlackRegion();}
忽然想試試19路標準棋盤下,雙方隨機落子黑棋能贏多少,測試函數:
template <BoardLen BOARD_LEN>void MCSimulator<BOARD_LEN>::TEST(){ int begin = clock(); int sum = 0; const int a = 10000; for (int i=0; i<a; ++i) { BoardInGm<TEST_LEN> b; b.Init(); auto &mcs = MCSimulator<TEST_LEN>::Ins(); int r = mcs.Simulate(b); sum += r; } int end = clock(); printf("time = %f\n", (float)(end - begin) / 1000000); printf("simulate complte.\n"); printf("average black = %f\n", (float)sum / a);}
19路的類比速度有點慢,28秒1w局。看來隨機落子的先行優勢並不明顯……
代碼:https://github.com/chncwang/FooGo