六子棋AI程序---核心講解

1.前言

筆者這裏想說一句:六子棋終於寫完了,啊~
不說了,先看戰績:
在這裏插入圖片描述這裏是沒有分出勝負的,但是,下棋時間先超過3分鐘的判輸。
在這裏插入圖片描述這裏是交換先後手,又下了一局,你可以認爲是對面棋藝不精,但看到最後你就明白了。
最近一個星期都在忙於升級自己的AI代碼,所以博客更新的比較慢,不過近期會加快更新的。下面我們進入正題。

2.遊戲規則

六子棋的規則與五子棋非常相似,玩家有黑白兩方,各持黑子與白子,黑方先行。採用19*19的棋盤。具體玩法:除了第一次黑方下一子外,之後白黑雙方輪流每次各下兩子(即第一步黑方下一子、然後白方下兩子、黑方下兩子、白方下兩子…)。直的、橫的、斜的連成6子(或以上)者獲勝。若全部棋盤填滿仍未分出勝負,且雙方各自下棋時間,則爲和局。否則若有一方時間先超過3分鐘,則直接判輸。

3.核心策略:貪心+博弈樹

(1)貪心策略

這也是筆者最早的一個版本,用的是貪心思想,即將子落在當前局面最佳落子點。思想很簡單,但問題來了,當前局面最佳落子點是哪一個?怎麼判斷?
筆者這裏做出詳細解答:我們假定己方爲白子,如果輪到白方落子時,發現棋盤上有五子相連,並且兩頭爲空(無子),那這兩頭是不是當前的最該落子的位置呢。如果沒有五子相連,四子相連,並且兩頭爲空,也是當前最該落子的位置(可落兩子,所以下四子相連的位置,可以成六子相連)。如果有兩對四子相連的白子相交於同一空位,那這個空位也是最該落子的位置。同理…
五子棋的話,想必絕殺贏得概率最大了,就是有兩對四子相連,對方根本堵不過來,那就是必贏了。六子棋也同樣如此,可是絕殺的棋型太多,怎麼可能全都列出,而且就算全都列出(感覺不太現實),我又怎麼判斷當前棋盤裏有沒有這種棋型呢?如果真想硬剛這方向的話,呃,只能說勸你善良,對自己好一點吧。
這裏我們換個思維,我們知道各式各樣的絕殺棋型,無非是一些小的模型平湊而成,比如幾個四子相連的模型,隨機的排列,那麼有些點必然就是必贏點。現在就是將問題簡化了,我們只要能找出一些小模型就行了(四子相連,五子相連,三子+一子…),這些小模型無非就是幾個棋子在六個連續的格子裏排列組合而已,要窮舉也是挺容易的。下面給出部分模型,其它模型就靠自己補充了。 在這裏插入圖片描述 這樣模型就寫出來了,在棋盤遍歷尋找模型時,在橫,縱,左斜,右斜四個方向上都要遍歷。可是如果找到了,又怎麼辦呢?我們前面部分說了,找最適合落子的點,既然是“最”,那麼它就一定有比較,有比較,就會有東西來衡量大小。這裏我們給每個格子賦上分值,挑選分值最高的就是最適合落子的位置了。那麼格子的分值怎麼來呢?對,既然我們已經有了模型,能匹配上模型的格子,如果是空格的話,都應該賦上分值,給上圖添加分值如下。
在這裏插入圖片描述
我們在建立模型的同時,給特定位置附上分值,如果匹配成功,則會在棋盤上賦上相應的分值。講到這裏,我們就可以給出模型代碼了。

struct Point { //點結構
	int x, y;
};
struct Model
{
	int status[6];//記錄每個格子的狀態,0爲黑,1爲白,2爲空
	int probability_p[6];//記錄各個格子的概率
};
//eg.對所有模型各個格子的分值賦值爲0,color代表某一方的顏色值,模型有很多個,所以定義模型數組model[n]
/*model[0].status[0] = 2;
	model[0].status[1] = color;
	model[0].status[2] = color;
	model[0].status[3] = color;
	model[0].status[4] = color;
	model[0].status[5] = color;
	model[0].probability_p[0] = 30;*/

留心的朋友可能注意到,有時候兩子相連會是三子相連的子集,而我們的分值還疊加了,這顯然是不對的。這裏做出的調整是,給每個格子加上“方向”,如果該格子在同一方向上匹配到多個模型,則挑選最大的分值,如果是不同方向,分值疊加。部分實現代碼如下:
在這裏插入圖片描述 意思是:先豎直方向上遍歷棋盤時,如果當前格子的方向不爲0,則給格子加上模型上的分值,如果當前格子的方向爲0,則比較原來分值,將較大的分值替換原來分值。最後將該格子的方向賦值爲當前豎直方向,其他方向同理。這樣,遍歷整個棋盤,找尋分值最高的點就是當前最適點了。
注意事項:計算分值一定要考慮雙方棋子,即己方四子相連落子概率很高,對面四子相連落子概率也很高,這也就是爲什麼建立模型時用的是color而不是具體顏色值。

(2)博弈樹

博弈樹是筆者後來加上的,正當筆者信誓旦旦的拿着自己的“貪心”六子棋AI程序找朋友挑戰時,結果那是個慘不忍睹…(如果你認爲是筆者輸的話,那你還真就猜對了…)。想想都可得知,只考慮當前最優解,對手稍微拐個彎子不就輸了麼。想用一句話形容當晚找朋友solo的自己,但想歸想。因爲早期的貪心策略是找當前最優解,如果沒有找到,則隨機落點,這棋技顯然是低得不行。當時輸了後是真想放棄貪心的那套代碼了,雖然花了三天時間,有點捨不得,但確實是好垃圾呀。迫於無奈,只能暫時將它擱置一邊,打算重寫一套。這就是接下來要講的重點,博弈樹了。
簡單來說,博弈樹的主要思想是,假設我走這,假設你走那,假設我走這,假設你走那…。就這麼循環往復,獲取未來棋盤的分值(是棋盤的分值,和貪心裏格子的分值不同),選出分值最高的走法,返回給當前。但是舉個例子,如果是20乘20的棋盤的話,如果深入四層,那麼可能的走法就有400乘399乘398乘397,也就256億種假設的走法吧。如果你對自己的計算機很自信的話,你可以試試,下面一段代碼需要多久才能運行完。

int i,j,k,m;
for(i=0;i<400;i++)
{
   for(j=0;j<400;j++)
   {
      for(k=0;k<400;k++)
      {
         for(m=0;m<400;m++)
         {
            cout<<""<<endl;
         }
      }
   }
}
cout<<"謝謝你走進我的世界"<<endl;

針對上面的問題,可以用“剪枝”來解決,但當時由於時間比較緊,筆者對剪枝還理解的不到位,又爲了減少運行時間,只用了兩層的博弈樹。其核心思想爲深度優先搜索,極大極小值搜索,遞歸。用圖來描述爲:
在這裏插入圖片描述 這裏我們一步步進行講解
首先:棋盤分值怎麼定,如果貪心策略明白的話,那麼棋盤分值也很好得出。同樣的模型,只是把每個點的分值改爲模型的分值,如果匹配成功則給當前棋盤加上模型的分值就可以了。
第二:我們博弈的起始點無需從棋盤的所有位置開始,比如說,如果棋盤上只有中心有一個棋子,輪到你下的時候,你會下在棋盤的四個頂角嗎,顯然不會。所以這裏我們只需要將棋盤上所有點周圍的點作爲可能的起始點就可以了,如下圖,周圍黃色的點即爲第一層可落子的點,通過大致縮小範圍可大大減小運行時間, 在這裏插入圖片描述第三:假設我們落在綠色點,那麼重複步驟二,確定下一步可能落子的點,如黃色區域所示。 在這裏插入圖片描述第四:假設我們落在紅點,則結束(如果想多深入幾層,也可再次重複步驟二),計算當前棋盤分值。 在這裏插入圖片描述講到這裏相信大家思路都比較清晰了,代碼也不難實現,感覺上就兩層for循環,一個計算當前棋盤分值的函數,和一個獲取可下點(黃色區域)的函數,每次獲取子結點分值最高的那個就行了。(貌似兩層和深度優先搜索沒啥關係,自己代碼還寫複雜了…)。

4.總結

前言的兩張圖片是用筆者的貪心+兩層博弈樹和六層博弈樹的對局(對面是正版,用了剪枝,所以運行時間7分鐘下完整個棋盤算挺快的了)。我們這兩者結合的好處就是,如果你對局的是用博弈樹的,可以耗它時間,讓它超時(如果對面剪枝不行的話)。如果你對局的是普通的貪心,那麼幾乎開頭幾句就絕殺了(兩層博弈樹的功勞),原來就有一局是和普通貪心比的,開局三步定勝負了,對面一直堵我,最後堵不完了,絕殺。如果想讓AI程序棋藝更好的話,可以上github好好學學剪枝,剪枝剪得好加上貪心節省時間,真的可以很秀的。最後,不管怎樣,終於不用被六子棋折磨了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章