第三章 資料結構及演算法
第五節 打牌演算法
在其他遊戲中,如圍棋、象棋…等,一旦先後手的關係決定,就會一直持續 到遊戲結束,橋牌和這些遊戲最大的不同點就是沒有一個固定的先後手關係,無 論是在換牌或者打牌步驟時,每一輪出牌較大者會獲得下一輪先出牌的權利,這 使得我們在做 Min-Max 法展開時需要有一些改變。在一般的 Min-Max 法展開中,
所有的節點分數都是站在我方的立場來看,我們在我方盤面時所有展開的子節點 取 Max,對方盤面時對展開的子節點取 Min,如圖 3-14。
5
-7 5
-7 3 5 7
-7 -9 2 3 5 -5 -6 7
3 -7 -9 -4 2 4 8 3 5 7 -5 -4 1 -6 8 7
max min
max
min
最終的分數
圖 3-14 Min-Max 法展開示意圖
圖 3-15 蜜月橋牌可能出現的先後手轉換
蜜月橋牌遊戲中,很難會發生某一方從頭到尾都是先手(每一回合都大於對 方),或者是都是後手的情況(每一回合都小於對方),所以勢必會出現 3-15 的情 況,因為根據不同的出牌選擇,造成子節點會是由不同玩家先出牌的現象,這樣 的情況無法直接取子節點分數最高或取子節點分數最低者來作為節點的分數。
在我設計的資料結構中,在展開的過程中不是以特定玩家來做盤面的評分,
而是以「先手」的角度來看這一局牌,子節點回傳的分數都是在子節點盤面持先 玩家的分數。例如:雙方各有八張牌時,排序為 0111101010001100,花色分佈為 王牌 6 張,其餘花色為 1、2、7 張,先手(排序中 1 的玩家)可以在這八磴牌中贏 得 4 磴,因此回傳分數為 4。我把其他的處理過程交給父節點來處理。
011110 王牌
1 花色 1
01 花色 2
0001100 花色 3
1 張 2張 7張
6張
圖 3-16 橋牌應用 Min-Max 法展開示意圖
在圖 3-16 中,父節點分數由子節點回傳值決定,當子節點盤面仍是持先手,
表示這一回合獲勝,故除子節點盤面回傳分數外,還要再加上 1 分才是真正的分 數。若子節點盤面為對方先手,回傳分數為對方得分,因雙方總得分等於總牌磴 數,level 減去對方得方即為己方得分。在這個架構下,不管是輪哪一方的盤面,
都是取最大值。
3.5.2 盤面展開及分支裁減
在這裡我們要說明為何有些分支是不需要展開,而又要如何找出這些分支,
以下面的實際例子來說明。
圖 3-17 是兩位玩家在打牌步驟時的牌以及排序組合,如同之前介紹過的資
料結構,其中 1 代表有出牌權的玩家擁有的牌在序列中的位置,0 代表另一位玩 家的牌在序列中的位置。
圖 3-17 雙方牌型組合
打牌時,先出牌的玩家 A 可以任意出牌,如果要求得最佳策略,就必須考慮 所有路徑,對每一張牌作展開搜尋,如圖 3-18。
圖 3-18 分支展開圖
但是事實上有些分支是不需要被展開的,如圖 3-17,我們只看黑桃花色的 部分,玩家 A 的牌有 A、J、9、5、3,玩家 B 的牌有 K、7,雙方的混合序列為
1011011。若玩家 A 選擇出♠J,玩家 B 可選擇的牌有♠K、♠7,出♠K 會使黑桃序列 變為11011,出♠7 會使序列變為 10111。而玩家 A 選擇♠9 時,玩家 B 可做的選擇 仍然一樣,而最後會形成的序列也是一樣。因為在♠J、♠9 之間並沒有玩家 B 的 牌,所以只要大於♠J 的牌,必然也會大於♠9,反之小於♠9,也會小於♠J,在玩 家 B 看來,♠J、♠9 兩張牌是等價的。因此我們得到一個結論,在相同花色的兩 張牌之間,若不存在對方的牌時,這兩張牌為等價,會有相同的展開結果,故不 需要重複做展開,利用這一點,我們可以減少展開的分支度。圖 3-19 為經過分 支裁減的結果,在這個例子裡我們減少了四個分支的展開。
圖 3-19 分支裁減後展開
承上例,若玩家 A 出了♠9,玩家 B 出了♠K,則下一回合玩家 B 會先出牌,而 原本的序列則是以玩家 A 為先手,這時只要對原本的序列取補數,就可直接將盤 面修正為以玩家 B 為先手的正確盤面。在我的程式中,因為採用了序列的結構來 記錄牌型,不僅在盤面資訊的傳遞上簡單,不需要複製整個牌組的陣列,只需要 傳遞兩個整數,在運算上也只需要簡單的 AND、OR、XOR 等指令就可以完成牌組 的展開。
最後,因為對手是人類,所不一定會照著電腦預計的最佳路徑來打牌,所以每一 回合都需要重新做一次搜尋,針對對手的走步重新制定最佳策略。
先手轉換
打牌程式展開搜尋虛擬碼如下:
int search (int order, int flower, bool first, int depth) { //order 紀錄排序狀況
//flower 為花色分佈 first 紀錄先手轉換 /*這裡採用了 min-max 的架構,所以在一開始時,min 要被初始化為無限大,max
要被初始化為無限小*/
}
max = maxmum (min[0],min[1],…);
return max;
}
上述程式碼是以遞迴的架構來完成展開的動作,當深度(depth)為 0 為展開 的最終節點,在打牌時如果有發生先手轉換(fiest == 1)的情況,則將序列取補 數以符合盤面情況,函式 testbit()會檢查先手所有可能出牌,也就是在序列中 為 1 的部份,再檢查後手所有合理的出牌,再以遞迴方式及 Min-Max 演算法找出 最佳路徑。