koba::blog

小林聡: プログラマです

麻雀の副露判断アルゴリズム(1)

電脳麻将 ver.0.6 では喰い仕掛けを実装した。基本的に向聴数が減るように鳴いていけばいずれ聴牌する*1のであるが、やみくもに鳴くと役なし聴牌となり和了できない状態になってしまう。実は以前から向聴数の減るときに鳴くという処理は実装済みなのだが封印してあったのだ。*2

役なし聴牌を回避するアイデアとして、特定の条件のときにのみに鳴くという方法が考えられる。例えば「役牌は鳴いてよい」、「幺九牌のないときに中張牌は鳴いてよい」などであるが、いかにも付け焼き刃な方法で一貫性がない。それより汎用的な方法として、

  • 特定の役に対する向聴数を計算し、その向聴数が減る場合に鳴く

という方法を思いついたので、それにしたがって実装することにした。

汎用の有効牌算出ルーチンの向聴数計算ルーチンを外部から指定できるようにする

汎用の向聴数計算ルーチンは Majiang.Util.xiangting()、有効牌*3算出ルーチンは Majiang.Util.tingpai()実装済みであるが、これを副露判断に適したものに置き換える。

まず、汎用の有効牌算出ルーチンの使用する向聴数計算ルーチンを外部から指定できるようにする。

Majiang.Util.tingpai = function(shoupai, xiangting) {
                                         // 第2引数に向聴数計算関数を追加
    var pai = [];
 
    if (shoupai._zimo) return pai;
 
    /* 指定のない場合はデフォルトの向聴数計算関数を使用する */
    xiangting = xiangting || Majiang.Util.xiangting;
 
    var n_xiangting = xiangting(shoupai);       // 向聴数計算関数を入れ替え
    for (var s of ['m','p','s','z']) {
        var bingpai = shoupai._bingpai[s];
        for (var n = 1; n < bingpai.length; n++) {
            if (bingpai[n] >= 4) continue;
            bingpai[n]++;
            if (xiangting(shoupai) < n_xiangting) pai.push(s+n);
                                                // 向聴数計算関数を入れ替え
            bingpai[n]--;
        }
    }
 
    return pai;
}

思考ルーチンに独自の向聴数計算ルーチン、有効牌算出ルーチンを追加する

クラス Majiang.Player に副露判定用の向聴数計算ルーチン、有効牌算出ルーチンを追加する。まずは門前手の向聴数だけ計算できるようにした。

/* 
 *  向聴数計算ルーチン
 */
Majiang.Player.prototype.xiangting = function(shoupai) {

    /* 門前手の向聴数を計算する */
    function xiangting_menqian(shoupai) {
        if (shoupai._fulou.filter(function(m){return m.match(/[\-\+\=]/)}).length)
                                                            return Infinity;
                                     // 副露牌がある場合は向聴数は無限大
        return Majiang.Util.xiangting(shoupai);
                                     // それ以外は汎用の向聴数計算ルーチンを使用
    }

    var x, min = Infinity;           // 向聴数の初期値は無限大
 
    x = xiangting_menqian(shoupai);
    if (x < min) min = x;

    return min;
}

/*
 *  有効牌算出ルーチン
 */
Majiang.Player.prototype.tingpai = function(shoupai) {
    var self = this;
    return Majiang.Util.tingpai(
                shoupai, function(s){return self.xiangting(s)});
                                    // 上で定義した向聴数計算ルーチンを使用
}

思考ルーチン内の向聴数計算ルーチン、有効牌算出ルーチンを入れ替える

さらに思考ルーチン内の向聴数計算ルーチン、有効牌算出ルーチンを上で定義したルーチンに入れ替えた。具体的には

  • Majiang.Player.prototype.select_fulou() (副露判断)
  • Majiang.Player.prototype.select_dapai() (打牌選択)

のルーチンをすべて入れ替えた。

ソースの差分は以下を参照。

これで準備はできたので、次回から役ごとの向聴数計算処理を実装する。

*1:実際には向聴数の変わらない鳴きも重要なのであるが、これは後回しにする

*2:副露した状態の試験をしたいときに封印を解いていた

*3:向聴数を減らす牌のこと