中國象棋軟體 引擎實現(四)搜尋演算法

2021-08-15 16:48:11 字數 3680 閱讀 8082

首先對於棋類遊戲存在一棵「博弈樹」——樹的每乙個結點代表棋盤上的乙個局面,對每乙個局面(結點)根據不同的走法又產生不同的局面(生出新的結點),如此不斷直到再無可選擇的走法(棋局結束)。

現在假定甲乙兩方下棋,甲勝的局面是乙個極大值(乙個正數),那麼乙勝的局面則是乙個極小值(極大值的負值),和棋的局面則是零值或是接近零的值。如此,則輪到甲走棋時他會選擇生成結點的值最大的走法,相反輪到乙走棋時他會選擇生成結點的值最小的走法(當然,搜尋會向下推導多步後選擇乙個分值對當前下棋方有利的著法)。這就是「最大-最小」的基本思想。這樣搜尋函式可以通過搜尋以當前局面為根結點、限定層數以內的整棵樹來獲得乙個最佳的著法。不幸的是,博弈樹相當龐大(它會成指數增長),因而搜尋整棵樹是一件相當費時的工作。

然而,下棋是乙個你來我往的交替進行並且相互「較勁」的過程,由於每一方都會盡可能將局面導向對自己有利而對對方不利的形勢。所以有些「暫時」看來很不錯的局面由於可能會產生很糟糕的局面因而根本沒有考慮的價值。所以當你看到某個局面有可能產生很糟糕的局面時(確切地說這裡的「很糟糕」是與之前所分析的情況相比較而言的),你應當立刻停止對其剩餘子結點的分析——不要對它再報任何幻想了,如果你選擇了它,則你必將得到那個很糟糕的局面,甚至更糟……這樣一來便可以很大程度上減少搜尋的工作量,提高搜尋效率。這稱為「樹的剪裁」。為了便於大家理解,下面我援引elephantboard的主頁上所翻譯的《alpha-beta搜尋》中的乙個「口袋的例子」,原文作者是bruce moreland ([email protected])。

口袋的例子:

比如你的死敵面前有很多口袋,他和你打賭賭輸了,因此他必須從中給你一樣東西,而挑選規則卻非常奇怪:

每個口袋裡有幾件物品,你能取其中的一件,你來挑這件物品所在的口袋,而他來挑這個口袋裡的物品。你要趕緊挑出口袋並離開,因為你不願意一直做在那裡翻口袋而讓你的死敵盯著你。

假設你一次只能找乙隻口袋,在找口袋時一次只能從裡面摸出一樣東西。

很顯然,當你挑出口袋時,你的死敵會把口袋裡最糟糕的物品給你,因此你的目標是挑出「諸多最糟的物品當中是最好的」那個口袋。

你很容易把最小-最大原理運用到這個問題上。你是最大一方棋手,你將挑出最好的口袋。而你的死敵是最小一方棋手,他將挑出最好的口袋裡盡可能差的物品。運用最小-最大原理,你需要做的就是挑乙個有「最好的最差的」物品的口袋。

假設你可以估計口袋裡每個物品的準確價值的話,最小-最大原理可以讓你作出正確的選擇。我們討論的話題中,準確評價並不重要,因為它同最小-最大或alpha-beta的工作原理沒有關係。現在我們假設你可以正確地評價物品。

最小-最大原理剛才討論過,它的問題是效率太低。你必須看每個口袋裡的每件物品,這就需要花很多時間。

那麼怎樣才能做得比最小-最大更高效呢?

我們從第乙個口袋開始,看每一件物品,並對口袋作出評價。比方說口袋裡有乙隻花生黃油三明治和一輛新汽車的鑰匙。你知道三明治更糟,因此如果你挑了這只口袋就會得到三明治。事實上只要我們假設對手也會跟我們一樣正確評價物品,那麼口袋裡的汽車鑰匙就是無關緊要的了。

現在你開始翻第二個口袋,這次你採取的方案就和最小-最大方案不同了。你每次看一件物品,並跟你能得到的最好的那件物品(三明治)去比較。只要物品比三明治更好,那麼你就按照最小-最大方案來辦——去找最糟的,或許最糟的要比三明治更好,那麼你就可以挑這個口袋,它比裝有三明治的那個口袋好。

比方這個口袋裡的第一件物品是一張20美元的鈔票,它比三明治好。如果包裡其他東西都沒比這個更糟了,那麼如果你選了這個口袋,它就是對手必須給你的物品,這個口袋就成了你的選擇。

這個口袋裡的下一件物品是六合裝的流行唱片。你認為它比三明治好,但比20美元差,那麼這個口袋仍舊可以選擇。再下一件物品是一條爛魚,這回比三明治差了。於是你就說「不謝了」,把口袋放回去,不再考慮它了。

無論口袋裡還有什麼東西,或許還有另一輛汽車的鑰匙,也沒有用了,因為你會得到那條爛魚。或許還有比爛魚更糟的東西(那麼你看著辦吧)。無論如何爛魚已經夠糟的了,而你知道挑那個有三明治的口袋肯定會更好。

「最大-最小」的思想再加上「對樹的剪裁」就是alpha-beta搜尋演算法的核心。

下面就是cchesssearch.h的**實現,其中涉及的「歷史啟發」和「著法排序」的具體實現會在下篇文章中給出。

// cchesssearch.h   

#include "historyheuristic.h"

#include "sortmove.h"

#include "cchessevaluate.h"

/// data define ///

int nmaxsearchdepth; // 最大搜尋深度

cchessmove cmbestmove; // 儲存最佳走法

/// function prototype

// 通過alphabeta搜尋+歷史啟發搜尋得到一部最佳著法並執行之

cchessmove searchagoodmove();

// alphabeta搜尋+歷史啟發,ndepth為搜尋深度,

// alpha初始為極小值,beta初始為極大值

int alphabeta_hh( int ndepth, int alpha, int beta );

// 判斷遊戲是否結束,若結束則根據當前下棋方返回相應的極值,否則返回0

// 當前下棋方勝則返回極大值,當前下棋方敗則返回極小值(下棋方追求極大值)

int isgameover( int fwhoseturn );

// 執行著法,返回ptto位置的棋子狀況。即若吃掉子返回被吃掉的子,沒有吃子則返回0

byte domove( cchessmove * move );

// 撤銷執行著法,恢復原位置的棋子狀況。ncchessid為原ptto位置的棋子狀況

void undomove( cchessmove * move, byte ncchessid );

// programmer-defined function

cchessmove searchagoodmove()

int alphabeta_hh( int ndepth, int alpha, int beta )

mergesort( movelist[ndepth], ncount ); // 對著法進行降序排序

int ibestmove = -1;

for( i = 0; i < ncount; i ++ )

if( alpha >= beta )

} if( ibestmove != -1 )

enterhistoryscore( & movelist[ndepth][ibestmove], ndepth ); //記錄歷史得分

return alpha;

}

int isgameover( int fwhoseturn )

}

return -maxvalue ; // 返回失敗極值(已驗證應為 -maxvalue )

} else // 輪到黑方下棋,只可能是黑將已經被吃

}

return -maxvalue ; // 返回失敗極值(已驗證應為 -maxvalue )

}

}

byte domove( cchessmove * move )

void undomove( cchessmove * move, byte ncchessid )

// end of cchesssearch.h

中國象棋軟體 引擎實現(七)測試程式

之前我們已經講了實現乙個中國象棋軟體的所有要素,本篇我們只是粗略地建乙個工程再新增一點檔案使得我們能看到程式的運 況如何。在介面完成之前,我先建了乙個win32控制台專案 學生朋友們對這個最熟悉也最容易理解 根據前面所講的我們已經有了cchessdef.h cchessmove.h cchessse...

中國象棋軟體 引擎實現(二)棋局表示

對於棋盤的表示當前比較先進的思想是 位棋盤 位棋盤 用於西洋棋非常便捷,因為西洋棋的棋盤正好有64個格仔,可以將乙個棋盤的資訊用乙個64位的變數來表示。其基本思想就是用位上的值是1或0來表示棋子在棋盤相應位置上的存在與否,這樣做的好處是可以通過位操作運算來加快局面評估和著法生成的速度。當用於中國象棋...

中國象棋主流象棋引擎分析

象棋旋風與佳佳象棋,從出現以來就廣泛吸引住了人們的眼球。在那個奇兵與大聖逐漸沒落的年代,旋風與佳佳的接連出現為象棋軟體的發展注入了新的活力。兩個軟體都採取了新的演算法,使得棋力相比過去的軟體有了較大幅度的增長,一時間風靡網路。這兩個軟體都開發了很多個版本,直到現在也沒有停息。但新版本採用了非常先進的...