はせらリバーシ、詰めオセロにビットボードを実装

はせらリバーシ
リバーシゲームをプレイしてみよう!

最近「はせらリバーシ」色々いじっていて、少し強くなったような気がするのと、ビットボードを実装して、高速化しました。

詰めオセロもビットボード化しました。15個空きのサンプルを貼ります(1手打つとCOMが14個空きを読み切ります)

白番。勝って下さい。

ビットボードは、以下のサイトのものをほぼそのまま使わせていただきました(感謝)局面にもよりますが、従来より2~5倍くらい速くなったと思います。

JavaScriptでのリバーシのビットボードの実装 - Qiita
#はじめにリバーシのビットボードはもう既に洗練されていますが(後にリンクを張ります)、これらの方法はほとんど全て64ビット整数が使える環境用です。JavaScriptは64ビット整数が使えないので…

一旦ほぼそのまま実装して動作を確認した後、少し改良しました。

どの位置に打っても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の作者の奥原氏のページなどが詳しいです。

reversi/othello - bitboard tricks
タイトルとURLをコピーしました