• 沒有找到結果。

BitBoard 產生炮之吃子步

第四章 暗棋與 BitBoard 資料結構

第七節 BitBoard 產生炮之吃子步

由於炮可以跳過多個格子,要用 BitBoard 來處理這個動作十分困難。西洋 棋的 BitBoard 中有一個名為 Magic Move-BitBoard Generation 之方法[5],利用

Hash 和 Bit Shift 來將 BitBoard 做轉換,來產生主教和城堡的走步,但很難實作。

本研究在嘗詴用此方法產生炮之吃子步的過程中,想出另一種新的切割與合併、

且簡單易懂又好實作之技巧,將炮之吃子步和 BitBoard 做出巧妙的結合。

本研究發現 BitBoard 有一種位移特性,表 4.1 中的任一列遮罩都可以經過 左移或右移 4 個位元變成另一列,例如第一列左移四個位元變成第二列:

0x0000000F << 4 = 0x000000F0

行也可以,第一行左移一個位元變成第二行:

0x11111111 << 1 = 0x22222222

有了這個特性後,就可以利用切割技巧,把跟炮同一行和同一列的資料擷 取出來處理。先取得炮在棋盤上 0~31 之位置後存到 src,把 src/4 取得列數(0~7 列)存到 r,src%4 取得行數(0~3 行)存到 c。首先處理跟炮同一列的炮位:

U32 x = ( (rank[r] & occupied) ^ (1<<src) ) >> (4*r)

將與炮同一列且有棋子的位置用「rank[r] & occupied」擷取出來,為了方 便處理,炮本身那格算沒有棋子,用「^ (1<<src)」消去,不管原本在哪一列,

全部用「>> (4*r)」移動到第一列統一處理,移動後只有最低的 4 個位元有效(第

這時炮可能會存在於四個位置:從右數來第一、二、三、四行,先處理在 第一行的狀況。當炮在第一行時,盤面總共有如圖 4.12 中的八種可能,圖中每 一列各為一種狀況,數字為索引,●代表任意一子,○代表炮位(為炮可以吃之

對方棋子的位置),空格代表該位置沒東西。

1 炮

2

3

4

5

6

7

8

圖 4.12 炮在第一行時可能有的炮位

找出○炮位的方法為:將前面得到的參數 x 依照程式碼 4.2 處理,先消去 最小位元(炮架),結果如圖 4.13 所示。若 x 不為零則代表下一個最小位元為炮

可以吃子的位置(狀況 5~8),若為零則代表炮在該列沒有子可吃(狀況 2~4):

if(x){

U32 mask = LS1B(x); //mask 為炮架的遮罩位置,讓 x 消去炮架。

return (x ^= mask) ? LS1B(x) : 0; //狀況 5~8 傳回 LS1B(x),狀況 2~4 傳回 0。

}else return 0; //狀況 1 傳回 0 程式碼 4.2 找出炮位的方法 1 ~ 4

5

6

7

8

圖 4.13 圖 4.12 在程式碼 4.2 中,消去炮架後可能得到的結果。

當炮在一列中間的兩個位置就更好處理,若炮在右邊數來第二行只需看他

左邊兩個位元是否都為 1,若是則最左邊的位置(8)就是炮位,否則沒有炮位(0):

return ( (x&12) == 12 ) ? 8 : 0;

若炮在右邊數來第三行,將上面的方法反過來處理即可:

return ( (x&3) == 3 ) ? 1 : 0;

若炮在右邊數來第四行,將 LS1B(x)改成 MS1B(x)即可(程式如下[5])。

U32 MS1B(U32 x){ // Most Significant 1 Bit (LS1B)函式 x |= x >> 32; x |= x >> 16; x |= x >> 8;

x |= x >> 4; x |= x >> 2; x |= x >> 1;

return (x >> 1) + 1; //可以取得一串位元中最左邊不為零的位元 }

處理行的吃子步就需要使用切割和合併的技巧,與處理列之吃子步時相同,

先用^(1<<src)削去炮並將八個 Bits 全部移動到第一行,如圖 4.14 將該行以炮為 中心切成上下兩邊,下半邊使用 CGenCL(U32 x)函式處理,和程式碼 4.2 一模 一樣,找出該串位元中第二小的位元即為炮位。上半邊使用 CGenCR(U32 x),

只需將程式碼 4.2 的兩組 LS1B(x)改為 MS1B(x)即可,即為該串位元中第二大 的位元,傳回值為圖 4.14 中○所表示的兩個炮位,將他們用 OR 合併即可。

● ●

● ●

● ●

● ●

接下來介紹切割的方法,炮在計算其一行之炮位時炮可能會存在於八個位 置:從上邊數來第一到第八個位置,如圖 4.14,所以需要 CGenC1(U32 x)~

CGenC8(U32 x)一共八個函式來處理切割的問題,CGenC1 由於炮在最上邊,執 行 CGenCL(x)處理下半邊即可:

U32 CGenC1(U32 x){

return CGenCL(x);

}

炮在第二個位置時,上半邊不可能有炮位,所以可以將上邊兩個位元(含炮 本身的位置)去掉再處理下半邊即可:

U32 CGenC2(U32 x){

return CGenCL(x&0x11111100);

}

炮在第三個位置時就需要切成兩邊再合併了,part 為上半邊的炮位,上半 邊需要兩個位元都有子時才可能有炮位,且炮位必定在上邊的位元,如圖 4.14,

下半邊直接把下邊五個位元取出後執行 CGenCL(x)即可:

U32 CGenC3(U32 x){

U32 part = 0;

if((x&0x00000011)==0x00000011) part |= 1;

return part | CGenCL(x&0x11111000);

}

第四個位置時上邊切出三個位元,下邊切出四個即可:

U32 CGenC4(U32 x){

U32 part = 0;

part = CGenCR(x&0x00000111);

return part | CGenCL(x&0x11110000);

}

第五~八個位置左右對稱,能以此類推簡單找出。

U32 CGenC5(U32 x){

U32 part = 0;

part = CGenCR(x&0x00001111);

return part | CGenCL(x&0x11100000);

}

U32 CGenC6(U32 x){

U32 part = 0;

part = CGenCR(x&0x00011111);

return part | CGenCL(x&0x11000000);

}

U32 CGenC7(U32 x){ return CGenCR(x&0x00111111); }

U32 CGenC8(U32 x){ return CGenCR(x); }

最後將一開始移動的位置還原後,把行與列的炮位做一次 OR 運算,再與

相關文件