再写围棋的MC模拟
有了棋串的数据结构后,落子就变得很高效了,接下来要生成随机棋步。
以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> PointIndex MCSimulator<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