
最近「はせらリバーシ」色々いじっていて、少し強くなったような気がするのと、ビットボードを実装して、高速化しました。
詰めオセロもビットボード化しました。15個空きのサンプルを貼ります(1手打つとCOMが14個空きを読み切ります)
白番。勝って下さい。
ビットボードは、以下のサイトのものをほぼそのまま使わせていただきました(感謝)局面にもよりますが、従来より2~5倍くらい速くなったと思います。
一旦ほぼそのまま実装して動作を確認した後、少し改良しました。
どの位置に打っても8方向分のビット演算をするのは無駄があるので、例えばa1に打つときは右、右下、下方向のみのビット演算を行うようにしました。
// 石を返す関数のテーブル、打つ位置によって呼ぶ関数を変える flipfunc = [ flip0a, flip0a, flip0b, flip0b, flip0b, flip0b, flip0c, flip0c, flip0a, flip0a, flip0b, flip0b, flip0b, flip0b, flip0c, flip0c, flip0d, flip0d, flip0 , flip0 , flip0 , flip0 , flip0e, flip0e, flip0d, flip0d, flip0 , flip0 , flip0 , flip0 , flip0e, flip0e, flip1d, flip1d, flip1 , flip1 , flip1 , flip1 , flip1e, flip1e, flip1d, flip1d, flip1 , flip1 , flip1 , flip1 , flip1e, flip1e, flip1a, flip1a, flip1b, flip1b, flip1b, flip1b, flip1c, flip1c, flip1a, flip1a, flip1b, flip1b, flip1b, flip1b, flip1c, flip1c ];
使うときは、flip0,flip1を呼ぶ代わりに、flipfunc[pos]を呼ぶという方法です。
posは盤面上の位置で、0~63で、例えばA1ならposは0で、flipfunc[0](つまりflip0a)を呼びます。
呼び元はこんな感じです。
if( turn == 'X' ){
if( pos < 32 ){
[f0, f1] = flipfunc[pos](board[1], board[0], board[3], board[2], 1<<pos);
}
else{
[f0, f1] = flipfunc[pos](board[1], board[0], board[3], board[2], 1<<(pos-32));
}
}
else{
if( pos < 32 ){
[f0, f1] = flipfunc[pos](board[3], board[2], board[1], board[0], 1<<pos);
}
else{
[f0, f1] = flipfunc[pos](board[3], board[2], board[1], board[0], 1<<(pos-32));
}
}
flip0aは、flip0から左、左上、上以外を削除したものです。
function flip0a(p1, p0, o1, o0, sq_bit) {
let f1 = 0;
let f0 = 0;
let mo1 = o1 & 0x7e7e7e7e;
let mo0 = o0 & 0x7e7e7e7e;
// 左
let d0 = 0x000000fe * sq_bit;
let t0 = (mo0 | ~d0) + 1 & d0 & p0;
f0 = t0 - ((t0 | -t0) >>> 31) & d0;
// 左上
t0 = sq_bit << 9 & mo0;
t0 |= t0 << 9 & mo0;
t0 |= t0 << 9 & mo0; let t1 = (t0 | sq_bit) >>> 23 & mo1;
t1 |= t1 << 9 & mo1;
t1 |= t1 << 9 & mo1;
let t = (t1 << 9 | t0 >>> 23) & p1 | t0 << 9 & p0; t = (t | -t) >> 31;
f1 |= t1 & t;
f0 |= t0 & t;
// 上 敵石にマスクはつけない
t0 = sq_bit << 8 & o0;
t0 |= t0 << 8 & o0;
t0 |= t0 << 8 & o0; t1 = (t0 | sq_bit) >>> 24 & o1;
t1 |= t1 << 8 & o1;
t1 |= t1 << 8 & o1;
t = (t1 << 8 | t0 >>> 24) & p1 | t0 << 8 & p0; t = (t | -t) >> 31;
f1 |= t1 & t;
f0 |= t0 & t;
return[f0, f1];
}
a1の場合は右、右下、下のみにするべきですが、このソースコードに書いてある日本語はでは、原作者様のイメージで上下逆なのです。
上下という概念は人間がどう認識するかの話なので、別に右下をA1にしても良いというか、最下位ビットをA1と考えても、H8と考えても良い訳です。私は原作者様のイメージのまま、最下位ビットをA1にしたので、普段使ってるA1~H8とは180度回転した座標になっています。
つまり、ビッグエンディアンとリトルエンディアンみたいな話…というか、そのものかも知れません。
こんな感じで、12種類の関数を呼び分けています。効果は…微妙でしたが、確実に速くはなりました。なぜ微妙かというと、もっと遅い部分があるからだと思います。
「打てる場所の一覧」を作る関数があるのですが、
/*打てる場所の一覧を作る*/
function makemoveables(board, turn){
var moveables = new Array();
if( turn == 'X' ){
[mob0, mob1] = mobility(board[0], board[1], board[2], board[3]);
}
else{
[mob0, mob1] = mobility(board[2], board[3], board[0], board[1]);
}
pos = 0;
length = 0;
while( mob0 != 0 ){
if( (mob0 & 1) == 1 ){
moveables[length] = pos;
length++;
}
mob0 >>>= 1;
pos++;
}
pos = 32;
while( mob1 != 0 ){
if( (mob1 & 1) == 1 ){
moveables[length] = pos;
length++;
}
mob1 >>>= 1;
pos++;
}
return( moveables );
}
moveablesという配列に、打てる場所(0~63の数字)の一覧を格納します。最初はfor分で必ず64回(32回×2)ループするように作ってしまったのですが、ループ回数減らせると気付いてこうしました(それで30%くらい速くなりました)
というか、打てる場所を数値に変換しないといけないか、というところに無駄があるような気がして考えたのですが、moveablesにビットマップを分解したものを格納するようにしてもループはするので、ほとんど変わらないか…
尚、ビットボードの話自体は、Booby Reversiの作者の奥原氏のページなどが詳しいです。

