牌の危険度計算アルゴリズム(1)

天鳳鳳凰卓の牌の危険度表 - koba::blog で天鳳鳳凰卓での牌の危険度と待ちの形の出現度を集計したが、その結果を 電脳麻将 の押し引きに反映する。

まず前回の2つの表の条件を掛け合わせて集計し直した。

単騎 双碰 嵌張 辺張 両面 合計
字牌 0.34% 0.73% 1.07%
生牌 0.98% 3.31% 4.29%
1枚切れ 0.68% 0.91% 1.59%
2枚切れ 0.12% 0.04% 0.15%
3枚切れ 0.00% 0.00% 0.00%
4枚切れ 0.00% 0.00% 0.00%
19牌 0.32% 0.70% 3.20% 4.22%
無スジ 0.30% 0.68% 3.69% 4.67%
スジ 0.44% 0.85% 1.29%
28牌 0.37% 0.69% 0.91% 3.61% 5.58%
無スジ 0.36% 0.66% 0.86% 4.14% 6.02%
スジ 0.46% 0.89% 1.24% 2.59%
37牌 0.32% 0.55% 0.99% 0.82% 3.98% 6.66%
無スジ 0.29% 0.52% 0.94% 0.77% 4.59% 7.10%
スジ 0.49% 0.76% 1.37% 1.12% 3.74%
456牌 0.38% 0.58% 0.72% 6.63% 8.31%
無スジ 0.40% 0.52% 0.72% 8.58% 10.21%
片スジ 0.34% 0.65% 0.69% 4.37% 6.05%
中スジ 0.42% 0.85% 1.04% 2.31%

改善内容

現在の電脳麻将は ベタオリのアルゴリズム - koba::blog で定義した表*1にしたがい牌の危険度を決定しているが、スジは考慮されているもののカベは考慮の対象となっていない。そこで先の待ちの形と危険度の統計値を参考に待ちの形ごとに評価値を定め、あり得る待ちの形の評価値を加算して危険度を決定することにした。

評価値を 単騎 = 1、双碰 = 2、嵌張 = 辺張 = 3、両面 = 10 とすれば以下の表のように統計値とほぼ一致する。

単騎双碰嵌張辺張両面合計
字牌 1 2 3
19牌 1 2 10 13
28牌 1 2 3 10 16
37牌 1 2 3 3 10 19
456牌 1 2 3 20 26

残り枚数、スジ、カベを考慮してあり得ないパターンを減算すると、

2枚切れ字牌待ち
双碰が否定されるので、単騎のみの 1点
スジ28牌待ち
両面が否定されるので、単騎 + 双碰 + 嵌張 の 6点
4が4枚切れの無スジ3待ち
両面と嵌張が否定されるので、単騎 + 双碰 の 3点

と危険度を決定できる。

プログラム修正

まず Majiang.SuanPai の牌の危険度を計算するメソッド .suan_weixian() を以下に修正する。

suan_weixian(p, l) {

    let [s, n] = p; n = +n || 5;

    let r = 0;                              // 初期値は 0
    if (this._dapai[l][s+n]) return r;      // 現物は 0点

    const paishu = this._paishu[s];

    /* 対子もしくは刻子 */
    r += paishu[n] >= 2 ? 3     // 生牌・1枚切れ: 単騎+双碰
       : paishu[n] == 1 ? 1     // 2枚切れ:       単騎のみ
       :                  0;    // 3枚切れ:       単騎、双碰 の可能性はなし
    if (s == 'z') return r;     // 字牌の場合は嵌張、辺張、両面の計算は不要

    /* n-2, n-1, n の順子 */
    r += n - 2 <  1                              ?  0   // 範囲外
       : Math.min(paishu[n-2], paishu[n-1]) == 0 ?  0   // カベ
       : n - 2 == 1                              ?  3   // 辺張
       : this._dapai[l][s+(n-3)]                 ?  0   // スジ
       :                                           10;  // 両面
    /* n-1, n, n+1 の順子 */
    r += n - 1 <  1                              ?  0   // 範囲外 
       : n + 1 >  9                              ?  0   // 範囲外
       : Math.min(paishu[n-1], paishu[n+1]) == 0 ?  0   // カベ
       :                                            3;  // 嵌張
    /* n, n+1, n+2 の順子 */
    r += n + 2 >  9                              ?  0   // 範囲外
       : Math.min(paishu[n+1], paishu[n+2]) == 0 ?  0   // カベ
       : n + 2 == 9                              ?  3   // 辺張
       : this._dapai[l][s+(n+3)]                 ?  0   // スジ
       :                                           10;  // 両面
    return r;
}

次に Majiang.Player のメソッド .select_dapai() で危険度を判定している部分を新しい尺度に変更する。

select_dapai(info) {

    /* …… */

    for (let p of this.get_dapai()) {

        /* …… */

        if (min < 10) {
            if (n_xiangting > 2              && weixian[p] >  min) continue;
            if (n_xiangting > 0 && ev <  400 && weixian[p] >  min) continue;
            if (n_xiangting > 0 && ev < 1200 && weixian[p] >=  10) continue;
        }

        /* …… */
    }

    /* …… */
}

従来は無スジを危険牌としていたが、今回は両面が残っている場合を危険牌とした。

評価

押し引きアルゴリズムの改善(3) - koba::blog と同様に、プログラム同士を1000戦 デュプリケート方式 で対戦させた。

結果 改善前 改善(1)
結果 割合 局収支 割合 局収支
和了 10.4% +6848 10.7% +6861
放銃 11.3% -6554 11.2% -6612
被ツモ 31.5% -2735 31.4% -2749
横移動 28.5% -69 28.6% -69
流局 18.3% -762 18.0% -750
平均 -1047 -1027

改善前 改善(1) 改善前 改善(1)
対戦数 1,000 1,000 総局数 10,480 10,488
1位率 .237 .237 和了率 .213 .215
2位率 .244 .250 放銃率 .133 .133
3位率 .248 .244 立直率 .231 .233
4位率 .271 .269 副露率 .342 .342
平均順位 2.55 2.55 平均打点 5,622 5,626

局収支は改善したが、平均順位はほぼ変化なし、和了率は若干向上した。まずはこの改善を採用することとする。

*1:この表自体は『科学する麻雀』の牌の危険度表を元にしている