[轉]12年前的作品──《美綠中國象棋》製作過程及算法簡介

這個遊戲是大學本科二年級時(1998年)修人工智能課程的功課 。這個遊戲的「棋力」並不高,主要是因爲沒有花時間在調整的工作上。比較滿意的部分是使用 OpenGL 做的使用者介面。本文將簡單介紹製作本遊戲的過程及當中用到的算法。你可以先下載(1049KiB)試試,但現時已找不到源碼了,將來找到的話再分享。

2010022718320779.jpg

製作過程

約在接到這項功課前的一個月,剛開始自學 OpenGL,因此便考慮利用 OpenGL 做使用者接口。

以前寫程式都是會先寫使用者接口,用來顯示程式的一些資料,之後再寫算法,遊遊戲也不例外。我利用Photoshop 及 Illustrator 繪製棋子及棋盤,再嘗試寫程序 (Visual C++ 6.0) 以立體方式顯示它們。由於太想試一試 OpenGL 的能力,在製作初期已加上了棋盤反射的效果。

之後便建構程序的重要資料結構,爲每種棋子設定可行走法,有些棋子是頗麻煩的,例如炮除了可四方向移動外,還能隔子吃棋。做好了這些規則,便加入使用者介面的輸入部分,可以進行人對人的遊戲。

至於人工智能部分,首先寫最簡單的搜尋演算法──極大極小搜索 (Minimax search) 。最初使用的評估函數(Evaluation function)只是雙方餘下棋子的數目的差,那麼電腦會避免自己的棋子被吃,又會盡量去吃對方的棋子,可謂已有一點「智能」了。 然後着手改良搜尋方法,使搜尋的速度提高。與此同時,參考了吳身潤先生的作品,去編寫評估函數。之後更加入了二千個棋譜走法 (book moves)。

接着得到各方好友的幫助,替它進行測試,以改善評估函數。我同時製作其餘的使用者接口部分,包括左上方的選單、右方的按鈕及棋局資料顯示。

整個製作過程歷時兩個月,幸好在功課期限前的一個星期是假期,能通宵達旦去完成。

算法簡介

這裏簡介這遊戲中用到的人工智能相關的算法。

遊戲樹

如同大部份中國象棋程序,這遊戲也是從遊戲樹(Game Tree)中找出最好的走法(Move)。

在遊戲樹中,每個節點代表遊戲的一個狀態(State),而每條邊是一個合法的走法。因爲象棋是雙方輪交替下棋(紅 -> 黑 -> 紅 -> ...),所以樹中每對相鄰的層階都是雙方各自可走的走法。下圖(來源)顯示了打井遊戲的部份遊戲樹。

2010022718363543.png

在打井遊戲中,由於最多是 9 步,它的遊戲樹深度爲 9,每一步之後,合法的走法變少了一個。因此,這個遊戲樹的總節點數目爲 1 + 9 + 9 * 8 + 9 * 8 * 7 + ... = 986410。假設有了這個遊戲樹,我們只要從樹中找出目前狀態的節點,再往下搜索到任何一個獲勝的葉節點(我方勝利終局),從當前節點走到該葉節點就是「必勝之道」,所以只要按「必勝之道」的第一步去走就會勝利了。事實上,寫一個不會輸的打井遊戲搜索 AI 只需要十數行代碼左右。

最小最大搜索

不過,中國象棋的遊戲樹是非常大的(雖然比圍棋小),不可把整個樹儲存或搜索至葉節點(終局除外)。因此,只能搜到某個深度,並在該深度的節點進行啓發評估 (Heuristic Evaluation),這估值反映了該節點棋局對我方的優勢。

由於在我方下棋的層數,我們會在每個節點選擇子節點中最大評估值,作爲本節點的評估值;在對方的層數,我們會假設他選擇最小啓評估值的節點;這就是最小最大搜索(Minimax Search)的原理。

爲了簡化程序,不用按層數選擇用 min 或 max,Minimax 通常在實現的時候會採用 Negamax 方式。其原理就是每層都是取下一層結果的負值的最大值。

所有 Minimax 搜索都可以做到安全的上下界剪枝,稱爲 Alpha-Beta 剪枝 (Alpha-Beta Pruning),這裏不詳述了。

以下是這遊戲中用到的方式:

AlphaBeta(alpha, beta, depth) {
	if (depth == 0)
		return Quiescence(alpha, beta);
	succ = generate all move from current board
	sort succ by estimate function
	for each node n in succ {
		makeMove(n);
		x = -AlphaBeta(alpha, beta, depth - 1);
		takeBack();
		if (x > alpha) {
			if (x >= beta)
				return beta;
			alpha = x;
			// update principal variation here
		}
	}
	if (no legal move)
		return -10000 + ply;
	return alpha;
}
平靜搜索

當搜索到最大深度的時候,有機會在下一步會產生很大的評估值變化(例如被吃子)。平靜搜索(Quiescence Search)就是在最大深度的時候繼續搜索,直至局勢變得穩定。

迭代深化

在Alpha-Beta 剪枝中,如果能儘快縮小上下界,將會減少搜索的節點數目。這就產生了迭代深化(Iterative Deepening) 的優化方法。相對於直接搜索深度爲N的樹,先搜索深度爲i,並用其結果來優化深度爲 i + 1 的搜索。

所謂結果,是指在Alpha-Beta 剪枝發生時去記錄Principal Variation,並利用這個來排序下迭代裏產生的走法。

遊戲樹中會有很多相同的節點。爲免重覆計算節點,這遊戲也使用了Transposition Table技術。

走法產生

在搜索中,產生合法走法佔很大的時間。因此,遊戲狀態的表示方式和走法產生是非常重要的。而產生的走法也用在使用者接口上:

2010022718444130.jpg

因爲沒有源代碼,我記憶中,這遊戲同時採用了"棋子->位置"及"位置->棋子"兩個數組去表達棋盤的狀態。例如車的移動要向四個方向產生走法,就可以用"位置->棋子"的數組去檢查某位置是否有其他棋子。

在開局時,會首先尋找開局庫有沒有記錄,這時候會用到一個更緊湊的棋盤表達方法。

評估方式

一個象棋的「智能」就在於其評估棋局的能力,上面提到的各種搜索優化只是用來加速搜尋(但在同等時限裏增加搜索的節點也能增加「棋力」)。評估方法花費的時間也大大影響整體速度,所以必須平衡評估函數的能力及花費時間。

這個遊戲採用的靜態評估分數爲(在結合其他評估時, 把這值乘以係數 10):

  • 士 6
  • 象 6
  • 馬 13
  • 車 52
  • 炮 22
  • 兵 2

如前文提及,只是按棋盤餘下的棋子,按這個分數來計算評估值,已可以有不少的「棋力」。這麼簡單的評估已令到計算機懂得「將軍抽車」,我這個象棋門外漢會輸給深度4的搜索。

我按照 [1],加入了單獨棋子位置的分數,及一些全局分析的分數,例如:

  • 車遲開步 -13
  • 車在馬後 -8
  • 炮在馬後 +8
  • 馬路被封 -4

要獲得更好的評估方式,可以靠人的經驗、分析專業棋譜、人工智能自動學習等等方法。

回顧感想

這個程式是我比較喜歡的作品,從外表(使用者接口)到內函(人工智能)都頗滿意。而在開發過程中也真正地學會了課堂和課本的知識。

在開發這個遊戲中,我也學習了象棋的一些規則,例如勝利條件不是吃了對方的將軍,而是對手沒步可走。但平手的規則並沒有完成。

沒想到十二年後,這個程序依然可以在Windows 7上運行自如(NT/2000/XP/Vista也沒問題),這要贊一下微軟。外表上,大概是用了比較簡單而獨特一點的設計,包括渲染和GUI等也不會感到很落後。不過,當年應該沒有使用系統時鐘,使現在棋子移動得太快,這算是一個缺憾吧。

在進入社會工作以後,我很少有機會自己一個人寫程序,編程的熱情也不如當天。但我會繼續在家裏編程,希望能回覆一點當日的熱血。

參考

  • [1] 吳身潤, 人工智慧程式設計:象棋, 旗標出版股份有限公司, 1996.
  • [2] George F. Luger, William A. Stubblefield, Artificial Intelligence: Structures and Strategies for Complex Problem Solving 2nd Edition, The Benjamin/Cummings Publishing Co. Inc., 1992.
發佈了52 篇原創文章 · 獲贊 0 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章