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

牌の危険度を評価する際に「スジをカウントする」のが最近のトレンドのようである*1。これは「終盤になり安全牌が増えればその分残った牌の危険度が上がる」という考え方で、相対危険度 のようなもの。牌の危険度計算アルゴリズム(1) - koba::blog で実装したカベを考慮した危険度は、巡目によって変化しない 絶対危険度 なので、

  • その牌の絶対危険度 / 絶対危険度の総和

で相対危険度を計算し、押し引きに利用することを試みる。

改善内容

相対危険度を求めるにあたり、牌の残り枚数で危険度を計算している現在のアルゴリズムを若干修正する必要がある。生牌 とは場に1枚も切れていない牌、つまり残り枚数 4 の牌のことであるが、自分が1枚だけ持っていて残り枚数 3 の牌もやはり生牌で危険度に差はない。つまり刻子・対子の危険度に関しては、自分の手牌にあるか否かも考慮する必要がある訳だ*2。また、字牌生牌は数牌生牌より危険とした方がよさそうなので、絶対危険度を 3 → 8 に変更した。

この補正をした上で絶対危険度の総和を分母とすれば相対危険度を求めることができる。

プログラム修正

Majiang.SuanPai の牌の絶対危険度を計算するメソッド .suan_weixian() の刻子・対子の危険度計算の際に自身の手牌を考慮するようにし、字牌生牌の危険度を 8 に変更する。

suan_weixian(p, l, c) {     // p が自身の手牌にある場合、c に真を設定

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

    /* …… */

    const paishu = this._paishu[s];

    /* 刻子・対子の危険度は自身の手牌も考慮して決定する */
    r += paishu[n] - (c ? 0 : 1) == 3 ? (s == 'z' ? 8 : 3)  // 生牌
       : paishu[n] - (c ? 0 : 1) == 2 ?             3       // 1枚切れ
       : paishu[n] - (c ? 0 : 1) == 1 ?             1       // 2枚切れ
       :                                            0;      // 3枚切れ
    if (s == 'z') return r;

    /* …… */

    return r;
}

Majiang.SuanPai に牌の相対危険度を計算するメソッド .suan_weixian_all() を追加する。

suan_weixian_all(bingpai) {     // bingpai は副露面子以外の自身の手牌

    let weixian_all;                            // 相対危険度を格納するハッシュ
    for (let l = 0; l < 4; l++) {
        if (! this._lizhi[l]) continue;         // リーチ者のみ危険度を判定する
        if (! weixian_all) weixian_all = {};    // ハッシュを初期化する

        /* 全ての牌の絶対危険度(weixian)とその総和(sum)を求める */
        let weixian = {}, sum = 0;
        for (let s of ['m','p','s','z']) {
            for (let n = 1; n < this._paishu[s].length; n++) {
                weixian[s+n] = this.suan_weixian(s+n, l, bingpai[s][n]);
                sum += weixian[s+n];
            }
        }

        /* 全ての牌の相対危険度を計算する */
        for (let p of Object.keys(weixian)) {
            weixian[p] = weixian[p] / sum * 100;    // 相対危険度を求める
            if (! weixian_all[p]) weixian_all[p] = 0;
            weixian_all[p] = Math.max(weixian_all[p], weixian[p]);
                                    // リーチ者が2人以上の場合は最大値を選択する
        }
    }
    if (weixian_all) return (p)=>weixian_all[p.substr(0,2).replace(/0/,'5')];
                                    // 相対危険度を返す関数を返す
}

評価

字牌生牌の危険度を変更したので、まずそれについてのみの効果を測定した。

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

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

リーチ後に字牌生牌が残っているケースが少なく*3、局収支、平均順位ともにほぼ変化がなかった。

次回はいよいよ相対危険度を押し引きに利用する。

*1:【麻雀守備講座】押し引き判断のベースとなる1/18理論を解説。捨牌読みが使えなくても危険度がわかる。 - YouTube

*2:今までは手牌の危険度だけを計算していたのでこの考慮は不要だった

*3:電脳麻将のアルゴリズムでは字牌から切るため