我的新博客:http://ryuzhihao.cc/
本文在我的新博客中的鏈接:http://ryuzhihao.cc/?p=601
前些天研究了一下棋譜2333,然後就順便寫了這個程序。整個程序是基於Qt開發,就UI而言毫無亮點,所以接下來的文章將主要介紹五子棋電腦AI的設計。可能這會是一篇非常長的博文。
在正文開始之前,首先貼一下程序的下載鏈接以及程序截圖~
一、程序預覽
1 下載地址:
https://pan.baidu.com/s/1nuJiPDJ
2 程序截圖:
圖1 五子棋程序截圖
圖2 一次着棋回合 (紅色爲鼠標位置,紫色框爲電腦上次着棋位置)
二、五子棋基本棋型
在設計AI之前,我們應該先告訴電腦何種棋型容易獲勝,什麼情形下應該進攻,什麼情形下應該採取防守策略。所以爲了設計更好的AI,需要先對五子棋棋型有些瞭解,在緊接着的章節裏,我在介紹棋型的同時,也會順便介紹一些算法的實現策略。
【約定】 _爲空,○爲敵方棋子,●爲己方棋子
① 連五:五顆棋子連在一起,獲得勝利。
●●●●●
② 活四:四顆棋子相連,同時兩端均爲空(即有兩個位置可以形成連五)。
當活四出現的時候,對方如果單純採取防守策略時,已經無法阻擋自己的勝利(除非對方採取進攻策略,一招制勝,我們的程序也要注意這一點)
_●●●●_
③ 死四:四顆棋子,但只有一個位置可以形成連五。
相比活四而言,死四的威脅要小的多,因爲這個時候對方只要跟着防守即可。但是死四出現時,其優先級應當比下面提到的活三要高(因爲活四雖能輕易破解,但是對於雙方都意味着一步結束比賽,故必須注意)。
_●●●●○
●_●●●
●●_●●
④ 活三:可以形成活四的三,有如下常見的幾種棋型:
活三棋型是進攻時最常見的棋型。因爲活三之後,如果對方不予理會,則可直接一手變成活四。因此當敵方活三出現時,需要進行防守。
_●●●_
_●_●●_
⑤ 死三:能夠形成死四的三。死三與活三相比,危險係數降低了不少,因爲死三即便不去防守,下一手也只能形成死四,我們仍然可以防守的住。
_●●●○ | _●_●●○ |
_●●_●○ | ●_ _●● |
●_●_● | ○_●●●_○ |
⑥ 活二:能夠形成活三的二。活二看似人畜無害,因爲它只下一手便能形成活三,等形成活三我們仍能防守。但其實活二其實很重要,因爲在開局階段,如果能夠形成較多的活二棋型,那麼當我們將活二變成活三時,就能將自己的活三綿延不絕,讓對手防不勝防。
_ _●●_ _
_●_● _
_●_ _●_
⑦ 死二:能夠形成眠三的二。
_ _ _●●○ | _ _●_●○ |
_●_ _●○ | ●_ _ _● |
三、着棋估值
着棋估值,是整個程序中最關鍵的一步。因爲估值方法,是教會電腦判斷如何根據當前棋盤形式,找到最適合的着棋位置的關鍵。而一個好的估值方法,也能大大提高電腦AI的獲勝概率。
事實上,如果不需要讓電腦AI具有預見未來若干步的本領,那麼只要實現這一步即可。並且,如果僅僅有着棋估值作爲AI判斷的考量標準時,電腦AI也能有不錯的表現(如果不小心,你會很容易輸給它)。
着棋估值,我們用這樣的函數原型描述它:
int envaluate ( Point p , Type who);
其中,參數p表示當前估值的棋盤座標點,who表示站在哪一方的角度進行估值(是玩家?還是電腦?)。
那麼,當我們不需要電腦有遠見的能力時,我們可以用如下代碼從整張當前棋盤中,找到最合適的落腳點:
/// 輪到AI的回合
void isTimeToAI()
{
Point bestPos1; // 最佳的進攻位置
Point bestPos2; // 最佳的防守位置
// 首先,分析採取進攻策略時的情況
// 當前棋盤中採取進攻策略的最高權重max1
int max1 = 0;
for(int r=0; r<size; r++) // 雙層循環遍歷棋盤
{
for(int c=0; c<size ;c++)
{
Point cur(r,c); // 當前查詢的位置
if(isNull(cur)) // 如果已經有棋子了
continue;
int value = envaluate(cur,COMPUTER);
if(max1 > value)
{
max1 = value;
bestPos1 = cur;
}
}
}
// 然後,分析採取防守策略時的情況
// 當前棋盤中採取防守策略的最高權重max2
int max2 = 0;
for(int r=0; r<size; r++) // 雙層循環遍歷棋盤
{
for(int c=0; c<size ;c++)
{
Point cur(r,c); // 當前查詢的位置
if(isNull(cur)) // 如果已經有棋子了
continue;
int value = envaluate(cur,PLAYER);
if(max2 > value)
{
max2 = value;
bestPos2 = cur;
}
}
}
// 從防守和進攻中找到最好的情況。
if(max1 >= max2) // 進攻
{
dropChessAt(bestPos1);
}
else // 防守
{
dropChessAt(bestPos2);
}
}
上述代碼,先從進攻的角度去尋找,又從防守的角度去尋找。如果進攻的優先級更高,那麼採取進攻策略;反之,採取防守策略。
那麼,判斷每個着棋位置的棋型envaluate(p, cur) 函數應該如何去定義?
根據第二章節的分析,我們可以設定如下棋型的權值順序:
連五 > 活四 > 死四 > 活三 > 活二 (略大於)死三 > 死二 > 其他棋型
那麼,我們在給定上述權值順序時,便可以得出如下的估值函數代碼:
int BoardAI::envaluateAt(Point p,int who) // 是哪一個玩家下在p位置
{
int value = 0;
int opposite = (who == PLAYER)?COMPUT:PLAYER; // 敵對方是誰
for(int i=0; i<8; i++) // 8個方向
{
//判斷是否存在 *11110 ("活四" 必勝 沒辦法去賭了)
if(getPointAt(p,i,1) == who && getPointAt(p,i,2) == who
&& getPointAt(p,i,3) == who && getPointAt(p,i,4) == who
&& getPointAt(p,i,5) == EMPTY)
{
value+=400000; // 40萬
}
// 判斷是否存在 21111* (死四A) 如果是己方則下子獲得勝利,對手的話要竭力去賭
if(getPointAt(p,i,1) == who &&getPointAt(p,i,2) == who
&& getPointAt(p,i,3) == who &&getPointAt(p,i,4) == who
&& getPointAt(p,i,5) == opposite)
{
value += 300000; // 30萬
}
// 判斷是否存在 111*1 (死四B)
if(getPointAt(p,i,-1) == who &&getPointAt(p,i,1) == who
&& getPointAt(p,i,2) == who &&getPointAt(p,i,3) == who)
{
value += 300000; // 30萬
}
// 自行添加其他棋型的判斷,這裏省略掉了,同上。
// 判斷是否存在 1*001(死二)
if(getPointAt(p,i,-1) == who && getPointAt(p,i,1) == EMPTY
&& getPointAt(p,i,2) == EMPTY && getPointAt(p,i,3) == EMPTY)
{
value +=100;
}
// 周圍如果已有棋子數目比較多的話,適當增加一下權值
value += (getPointAt(p,i,-1)== who
+getPointAt(p,i,-1)==opposite)*25;
}
return value;
}
在上面的代碼中,出現了一個函數:
int getPointAt (Point p, int dir, int offset);
其中,p爲當前探測的中心點,dir爲探測方向,Offset是距離p的偏移量,返回值爲該點的棋子類型(空、白棋、黑棋)。通過這個函數,我們可以藉助查詢距離p任意方向,且任意長度的點的類型。其具體的實現如下:
QPoint m_offset[8] = {
QPoint(0,-1),QPoint(1,-1),QPoint(1,0),QPoint(1,1),
QPoint(0,1),QPoint(-1,1),QPoint(-1,0),QPoint(-1,-1)
};
int BoardAI::getPointAt(Point p, int dir, int offset)
{
int r = p.row;
int c = p.col;
r = r + offset*m_offset[dir].y();
c = c + offset*m_offset[dir].x();
if(r<0 || c<0 || r>=BOARDSIZE || c>=BOARDSIZE)
return OUTRANGE;
return board[r][c];
}
四、有遠見的電腦AI?
通過前面的分析,我們已經能夠得到一個還不錯的AI了,或許我們也可以稱之爲一個不錯的棋手。但是現實中遇到的高手,大多能夠預見未來的若干步,並分析出當前最佳的策略。一則,可以避免未來可能發生的槽糕的情形;二則,可以爲未來可以構建的奇招打下基礎。
那麼,應該如何實現這樣有遠見的AI呢?
這裏,我們採用構建博弈樹的方式,選擇能夠導致未來最佳情形的策略。所謂博弈樹的構建,其實是以當前棋局爲根節點,然後下一步,我們可能在當前的任意一個空位着棋,那麼生成相應數目的葉節點(即每個葉節點,是我們在其父結點的基礎上,着下一棋的結果)。
那麼這樣,我們重複多次之後,就有可能生成如下的博弈樹:
這裏,我們只需要簡單的遞歸即可實現這個步驟。我們只需分析每個葉節點的權值(也就是未來幾步的情形),從中選取最好的情形,並按照這個策略着棋即可。
當然,我們可能會遇到一些可能的情況。比如中間某步時,敵方/己方獲得勝利,那麼我們可以賦這種情形一個較大的權重,但是仍要繼續遍歷,因爲有可能剩餘的步驟裏,敵方/己方可能在較短的路徑長度(PL)結束遊戲。