koba::blog

小林聡: プログラマです

麻雀の打牌選択アルゴリズム(3)

麻雀の打牌選択アルゴリズム(1) - koba::blog麻雀の打牌選択アルゴリズム(2) - koba::blog で待ち受けを広くする打牌はある程度選択できるようになったのだが、孤立牌の打牌に問題が残っていた。

例えば

一萬八萬八萬三筒三筒四筒六筒七筒八筒九筒一索一索三索赤五萬

の牌姿の場合、打 一萬赤五萬 ともに 八萬三筒五筒一索二索 の5種14枚の待ちとなるのだが、赤ドラかつ中張牌でくっつきの可能性が高い 赤五萬 を打牌してしまう(評価が同じ場合手牌の右側から切るため)。

今回は牌の評価値を求めることで中張牌やドラを残すよう改善する。

牌の評価値は以下とした、

  1. 搭子となる枚数を評価値とする
  2. ドラの評価値は2倍とする
  3. 役牌の評価値は2倍とする

例えば 發 の場合、搭子となり得るのは 發 1種3枚なので3点、かつ役牌なので2倍として6点となる(ドラの場合はさらに2倍して12点)。

赤五萬 の場合、三萬四萬六萬七萬 の各4枚と 五萬 の3枚で搭子となり得るので19点。さらに赤ドラでもあるので2倍して38点。

これにさらに残り枚数を考慮して牌の評価値を求めるメソッドをMajiang.SuanPaiに追加した。

Majiang.SuanPai.prototype.paijia = function(p) {

    function weight(s, n) {
        if (n < 1 || 9 < n) return 0;
        var rv = 1;
        for (var baopai of self._baopai) {
            if (s+n == Majiang.Shan.zhenbaopai(baopai)) rv *= 2;
        }
        return rv;
    }

    var self = this;

    var rv;
    var s = p[0], n = p[1]-0||5;

    if (s == 'z') {
        rv = this.paishu(s+n) * weight(s+n);
    }
    else {
        var left   = (1 <= n-2)
                   ? Math.min(this.paishu(s+(n-2)), this.paishu(s+(n-1))) : 0;
        var center = (1 <= n-1 && n+1 <= 9)
                   ? Math.min(this.paishu(s+(n-1)), this.paishu(s+(n+1))) : 0;
        var right  = (n+2 <= 9)
                   ? Math.min(this.paishu(s+(n+1)), this.paishu(s+(n+2))) : 0;

        rv = left                    * weight(s, n-2)
           + Math.max(left, center)  * weight(s, n-1)
           + this.paishu(s+n)        * weight(s, n)
           + Math.max(center, right) * weight(s, n+1)
           + right                   * weight(s, n+2);
    }

    if (p[1] == '0')                   rv *= 2;
    if (p == 'z'+(this._zhuangfeng+1)) rv *= 2;
    if (p == 'z'+(this._menfeng+1))    rv *= 2;
    if (p.match(/^z[567]/))            rv *= 2;
    rv *= weight(s, n);

    return rv;
}

ドラ 六萬三萬 残り3枚、四萬 残り2枚、五萬 残り1枚、六萬 残り4枚、七萬 残り3枚の状況で 赤五萬 を評価した場合、

  • 三萬四萬 で有効な*1搭子ができる枚数(left)は 2
  • 四萬六萬 で有効な搭子ができる枚数(center)は 2
  • 六萬七萬 で有効な搭子ができる枚数(right)は 3

となる。
weight() はドラによる重みづけで、上記の場合 weight('m', 6) は 2、他はすべて 1 となるので評価値は、

  • ( (2 x 1) + (2 x 1) + (1 x 1) + (3 x 2) + (3 x 1) ) x 2 x 1 = 24

となる。

この孤立牌評価機能を組み込んだアルゴリズムで前回同様1000戦自動対戦させた結果は以下の通り。

対戦数 1,000総局数 9,631
1位率 .289和了 .233
2位率 .267放銃率 .158
3位率 .230立直率 .446
4位率 .214副露率 .000
平均順位 2.37平均打点 7,097

和了出現回数出現率 和了出現回数出現率
ドラ 1,049 46.81% 翻牌 160 7.14%
赤ドラ 1,094 48.82% 平和 577 25.75%
裏ドラ 935 41.72% 断幺九 289 12.90%
立直 2,226 99.33% 一盃口 90 4.02%
ダブル立直 3 0.13% 三色同順 45 2.01%
一発 678 30.25% 一気通貫 12 0.54%
門前清自摸 575 25.66% 七対子 168 7.50%

立直率 43.7% → 44.6%、和了率 22.7% → 23.3%、放銃率 15.4% → 15.8%。中張牌を残すことで聴牌効率が上がったがその分放銃も増えた感じ。和了役ではドラの使用枚数が増え、断幺九も増えている。このため平均打点が 6,629 → 7.097 とアップした。平均順位も 2.45 → 2.37 と大幅に上昇したのでこの改良はうまくいったようである。このアルゴリズムを ver.0.4 に採用した。

今回の差分はこちら

*1:面子となり得るという意味