名稱:四子棋終結者 (中文版) 簡介:青衫(邱奕南)於2021年仿照Pascal Pons演算法所撰寫的四子棋遊戲。由於Pascal Pons的演算法可得知任一四子棋局面的勝負,因此使用本程式即可輕鬆打敗其他無法 全面展開的四子棋遊戲。雖然程式本身也可下棋,但因為不敗,所以比較缺乏娛樂性, 通常做為著棋參考使用。 保護:無 說明:(作者-青衫) 四子棋已證明為先手必勝,本遊戲採用的Pascal Pons演算法,可參考網址: http://blog.gamesolver.org。 以下針對該演算法的教學網頁,做一些簡單的說明。 part1為一些簡介,不重要。part2提供了一些測試資料與其分數,可用來驗證程式的正確性 與效率性。如果想要自己寫程式,最好下載並使用這些測試資料。 part3開始進入主題。早期的棋類遊戲,多半採用Game Tree展開搜尋法,對於簡單的棋類遊 戲非常有效。較複雜的棋類遊戲,效果就很有限,例如虞希舜的象棋大師3將族,便是採用此 一方法。這個方法的核心主要有兩點,剛好對應part3和part4。 (part3)MinMax演算法:棋類是一來一往的遊戲,對敵人分數很高的著點,對我方便不利, 因此在展開搜尋時,在我方著子時要找最高分,在敵方著子時要找最低分,所以名字才叫MinMax。 而為了避免要判斷回傳最高分或最低分,通常會將對手方回傳的分數值轉負,如此各層只要 找最高分即可。 (part4)Alpha-Beta裁剪演算法:這是Game Tree搜尋法的精華所在,利用已知的分數,裁剪 掉已經不可能更好的著子變化。例如已經搜尋到下某個位置在第5手便會獲勝,那麼其他著子 只要超過5手還未分勝負的,便不用再往下展開了。這個演算法的重點,在於如何定義分數, 以及如何評估目前局面可能的最高分數,這關係到裁剪收斂的速度,也就是搜尋的效率。目 前關於四子棋的所有論文,都是採勝利點距離盤尾的剩餘手數做為分數,因此愈快獲勝,距 離盤尾就愈遠,分數也就愈高。評估目前局面可能的最高分數,基本上便是42-已著子數(7x6 的棋盤最多42子)。 標準Game Tree搜尋演算法的寫法為: minmax(局面,目前局面已知的分數區間) { if 局面已知為勝、負或和局 then 回傳分數 評估目前局面可能的最高分,若不會更好便返回 針對目前局面另一方可下的子,一一展開呼叫minmax取得分數轉負,同時更新已知的分數區間下限 回傳前述展開後的最高分 } (part5,part10)Game Tree搜尋的速度要快,就要儘早大幅限縮目前局面已知的分數區間, 以便濾除大部份無須展開的著點。對於四子棋而言,便是儘量先展開可能獲勝的著點。part5 說的是儘量從中間著點先展開,part10則是加上能形成【等四】情況時優先(請參考四玲瓏 的四子棋著手提示)。就這麼簡單的兩個方式,基本上就可以讓整個演算法,在可等待的時 間內,展開完四子棋某局面的分數計算。 (part7)同形局面暫存表:這是Alpha-Beta外,另一個很有效的裁剪法。因為四子棋同形反 覆的情況非常多,例如下了位置abcd四子的情況,那麼下列三種著子順序(即搜尋次序)都 會導致相同的局面: adcb cbad cdab 以九子為例,下了九子的順序變化共有7^9=40353607,約4千萬種,實際上只有539062,大約 50萬個不同局面,只佔了1%多而已,其重複率非常之高。因此可以將已計算好的局面暫存起 來,如果遇到同形局面,便直接取用其回傳值,這樣就不用再往下展開搜尋。至此四子棋的 搜尋法,大抵已經成形,一個局面的分數計算已可在數分鐘內完成。後面的部份都只是一些 小改良而已,加速的幅度已不像前面的方法,動輒數十倍或數百倍以上之多。 (part8)分數區間二分搜尋法:因為我們要在可能的分數區間中(-42~42),找出目前局面 的實際分數。一種做法是採用二分搜尋法,取區間中間值,以只有1分區間的方式進行搜尋, 以得知實際分數是高還是低,然後限縮為一半的分數區間。反覆進行以得到最後的值。這種 方法對於複雜的局面會比較有幫助。 (part6)說明如何利用一個64位元長整數來表示一個四子棋的局面,可同時提升執行效率與記 憶體效率。 (part9)採用避免下會輸的棋,取代找會下贏的棋,如此可省下一層的搜尋,大抵可加快一倍。 (part11,part12)同形暫存表的小改良,只能加快一些些。 以該教學網頁寫出來的程式,評估第一著子後的局面分數,大約需要2~3分鐘,顯然不適合 用在即時互動的遊戲上。也就是說,原作者還有一些沒有寫出來的東西,必須試著將它補齊: 1.局面分數緩存表(Cache) 和同形局面暫存表類似,只不過同形局面暫存表是記錄在某個區間分數範圍內的回傳值,並 非實際分數,這裡記錄的是實際展開後所得的分數。也就是說,玩家在排某個著手變化時, 可能會來來回回的切換,此時已經展開計算過的著手局面,如果有記錄下來,便不用再次展 開了。這在人機互動上是個很有效的加速方法。 這裡要再多提的一點是,同形局面還包括了對稱同形。也就是說,第一手下第2欄,跟第一手 下第5欄,其結果是相同的,只是著子相互對稱而已。因此用來表示局面的key值,與經過對 稱計算後的key值,其實是一樣的,我們可以取其最小值來表示,如此緩存表可提升一倍的效 率。同形局面暫存表不建議這麼做,因為key值對稱計算需要多耗時,在大量展開計算時很可 觀,而多合併的一個同形局面遇到機率其實很低,加速的效果難以抵掉多出的耗時,總而言 之就是不划算。 key值的對稱計算程式寫法為(該網頁的程式裡沒有): typedef unsigned __int64 uint64; inline uint64 Mirror(uint64 data) { return ((data & 0x000000000000007f) << 42) | ((data & 0x0001fc0000000000) >> 42) | ((data & 0x0000000000003f80) << 28) | ((data & 0x000003f800000000) >> 28) | ((data & 0x00000000001fc000) << 14) | ((data & 0x00000007f0000000) >> 14) | (data & 0x000000000fe00000); } 2.內定局面分數記錄表 前面有說過,第一次展開計算第一著子的局面分數,需要2~3分鐘,對於玩家而言,基本上 是無法忍受的等待時間。對於這部份,我們可以將一些較為耗時的局面分數事先計算出來, 並加以記錄。如此玩家在使用時,便可以直接取用這些已事先計算好的分數,而不用再展開 計算。 (1)前幾手的局面:由於需時較久的,大部份都是手數較少的局面,因此可將前n手的所有局 面分數全部都先計算出來,並加以記錄。以取前10手為例,總共有330M個變化(3億多), 產生出約120萬個局面,全部計算完費時約3天。從第11手開始,最慢大約可在4秒內計算 完一個局面。但一次要計算7個著手局面,仍可能長達10幾秒以上,對於玩家而言,還是 有點過慢。而再往下展開第11手以上,局面變化數愈來愈多,但用時降幅卻愈來愈小,很 難達到真正快速的要求,因此我們必須另找其他方法才行。 (2)找展開節點數較多的局面:真正費時較久的,其實是展開節點數較多的局面,因此我們 必須找出展開節點數較多的局面,計算其分數並記錄下來。這種方式可記錄其他計算較久 的局面,而避免全面展開計算並記錄龐大的局面資料。於是我們便從第11手開始針對各種 變化,進行第一次(無暫存資料)的展開計算,找出超過一定節點數以上的局面,並記錄 其分數,同時再往下一層找出可能仍超過該節點數的子局面。依據實測的數據資料,0.1 秒大約可以展開1M個節點,如此計算7手可在1秒內完成,是個比較適合的門檻值。展開後, 共約有1315萬個局面,費時約9天。 最後實測結果,採用前述的局面分數記錄表,整個棋局分析的操作過程非常順暢,幾乎感覺 不到分數計算的停頓。至此,才算是實用的四子棋程式。 最後再說明一下,次著評估分數的校正。因為前面所謂的局面分數計算,是指下了棋後,該 局面的勝負分數,和次著的評估分數仍有一點小差異,特別是即將分出勝負時(實作後便會 知道)。以下為實際的次著評估分數流程: 對於要評估的7種著手 { if 該欄已滿 then 略過 else if 下了後我方即勝 then 直接計算分數(正分) else if 下了後對手次著即勝 then 直接計算分數(負分) else if 下了後我方次著必勝 then 直接計算分數(正分) else 呼叫計算函數解出下該子後的分數並轉負 }