koba::blog

小林聡: プログラマです

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

麻雀の打牌選択アルゴリズム(8) - koba::blog までのアルゴリズムで打牌の選択に打点を考慮した評価値*1を使用するよう修正したが、今回はその評価値を副露判断にも使用するよう改良する。

副露判断に評価値を使用することで以下の効果が期待できる。

  • シャンテン戻しや向聴数の変わらない鳴きが可能になる
  • 打点が極端に下がる鳴きをしなくなる

get_gang_mianzi() の共通化

チーできる面子を取得する関数 get_chi_mianzi()、ポンできる面子を取得する関数 get_peng_mianzi() はすでに共通化済みだが、カンできる面子を取得する関数 get_gang_mianzi() もこの機会に共通化しておいた。

function get_gang_mianzi(shoupai, p) {

    /* Majiang.Player.prototype.get_gang_mianzi を元に、任意の手牌を入力と
       できるよう修正 */
}

Majiang.Player.prototype.get_gang_mianzi = function(data) {

    if (this._paishu == 0) return [];
    if (this._baopai.length == 5) return [];
    
    if (data) {
        var d = ['','+','=','-'][(4 + data.l - this._menfeng) % 4];

        /* 共通化した get_gang_mianzi() を呼出す */
        return get_gang_mianzi(this._shoupai, data.p.substr(0,2)+d)
    }
    else {
        /* 共通化した get_gang_mianzi() を呼出す */
        var mianzi = get_gang_mianzi(this._shoupai);
        
        if (this._lizhi[this._menfeng] && mianzi.length) {

            /* リーチ後は送り槓と待ちの変わる槓を禁止する */

        }
        return mianzi;
    }
}

評価値のキャッシュをクリアするタイミングの変更

今までは打牌選択を開始するときにキャッシュをクリアしていたが、これでは副露判断の際に間違ったキャッシュを使用してしまうので

  • 自分の自摸の直後(この後に打牌選択がある)
  • 他者の打牌の直後(この後に副露判断がある)

のタイミングでキャッシュをクリアするよう変更した。

Majiang.Player.prototype.zimo = function(data, callback, option) {

    /* ………… */
    
    this._eval_cache = {};                      // キャッシュをクリア
    this.action_zimo(data, callback, option);   // この関数の延長で打牌選択

    /* ………… */
}

/* ………… */

Majiang.Player.prototype.dapai = function(data, callback) {

    /* ………… */

        this._eval_cache = {};                  // キャッシュをクリア
        this.action_dapai(data, callback);      // この関数の延長で副露判断

    /* ………… */
}

/* ………… */

Majiang.Player.prototype.select_dapai = function() {

    /* キャッシュをクリアする処理を削除 */
}

副露判断に評価値を使うよう修正

副露判断に評価値を使うよう修正する。2向聴以降は評価値が計算できる*2ので、鳴く前より鳴いた後の評価値が高ければ鳴くという判断とする。これにより

  • シャンテン戻しとなる鳴き
  • 向聴数の変わらない鳴き

も候補となるが、3向聴戻しとなる場合は評価値が計算できないため対象外とする。

Majiang.Player.prototype.select_fulou = function(data) {

    /* リーチ者がいる場合は副露しない */
    if (this._lizhi.filter(function(x){return x}).length > 0) return;
    
    /* 汎用の向聴数算出ルーチンで現在の向聴数を求める */
    var n_xiangting = Majiang.Util.xiangting(this._shoupai);
    
    if (n_xiangting < 3) {      // 2向聴以降の場合は評価値で副露判断する

        /* カン・ポン・チーすべてについて可能な副露面子の一覧を取得する */
        var mianzi = this.get_gang_mianzi(data)
                        .concat(this.get_peng_mianzi(data))
                        .concat(this.get_chi_mianzi(data));
        if (! mianzi.length) return;
        
        var fulou;
        var paishu = this._suanpai.suan_paishu_all();

        /* 現在の評価値を計算する */
        var max    = this.eval_shoupai(this._shoupai, paishu);

        /* すべての可能な副露後の評価値を計算し、現在の評価値を超える最良の
           評価値となる副露を選択する */
        for (var m of mianzi) {
            
            var new_shoupai = this._shoupai.clone();
            new_shoupai.fulou(m);
            if (Majiang.Util.xiangting(new_shoupai) >= 3) continue;
                                            // 3向聴戻しとなる副露は選択しない

            ev = this.eval_shoupai(new_shoupai, paishu);
            
            if (ev > max) {
                max = ev;
                fulou = m;
            }
        }
        return fulou;
    }
    else {                      // 3向聴以前の場合は向聴数のみで副露判断する

        /* 役を考慮した向聴数算出ルーチンで向聴数を求めなおす */
        n_xiangting = this.xiangting(this._shoupai);
 
        /* シャンテン戻しとならなず三槓子が成立する場合は大明槓する */
        for (var m of this.get_gang_mianzi(data)) {
            var new_shoupai = this._shoupai.clone();
            new_shoupai.fulou(m);
            if (this.xiangting(new_shoupai) == n_xiangting
                && new_shoupai._fulou.filter(function(mm){
                        return mm.match(/^[mpsz](\d)\1\1.*\1.*$/)}).length >= 3)
            {
                return m;
            }
        }

        if (n_xiangting == 0) return;   // テンパイ時は鳴かない

        /* 向聴数が進む場合はポン・チーする */
        for (var m of this.get_peng_mianzi(data)
                            .concat(this.get_chi_mianzi(data)))
        {
            var new_shoupai = this._shoupai.clone();
            new_shoupai.fulou(m);
            if (this.xiangting(new_shoupai) < n_xiangting) return m;
        }
    }
}

暗槓・加槓の判断に評価値を使うよう修正

暗槓・加槓についても同様に評価値を使うよう修正した。

Majiang.Player.prototype.select_gang = function() {

    /* 汎用の向聴数算出ルーチンで現在の向聴数を求める */
    var n_xiangting = Majiang.Util.xiangting(this._shoupai);

    /* リーチ者がいて自身がテンパイしていない場合は暗槓・加槓しない */
    if (this._lizhi.filter(function(x){return x}).length > 0
        && n_xiangting > 0) return;
 
    if (n_xiangting < 3) {      // 2向聴以降の場合は評価値で判断する

        var paishu = this._suanpai.suan_paishu_all();

        /* 現在の評価値を計算する */
        var ev = this.eval_shoupai(this._shoupai, paishu);

        /* すべての可能な暗槓・加槓後の評価値を計算し、現在の評価値を超える
           場合は暗槓・加槓する */
        for (var m of this.get_gang_mianzi()) {

            var new_shoupai = this._shoupai.clone();
            new_shoupai.gang(m);
            if (Majiang.Util.xiangting(new_shoupai) >= 3) continue;
                                                        // 3向聴戻しはしない
 
            if (this.eval_shoupai(new_shoupai, paishu) > ev) return m;
        }
    }
    else {                      // 3向聴以前の場合は向聴数のみで判断する

        /* 役を考慮した向聴数算出ルーチンで向聴数を求めなおす */
        n_xiangting = this.xiangting(this._shoupai);

        /* シャンテン戻しとならない場合は暗槓・加槓する */
        for (var m of this.get_gang_mianzi()) {

            var new_shoupai = this._shoupai.clone();
            new_shoupai.gang(m);

            n_xiangting = this.xiangting(this._shoupai);
            if (this.xiangting(new_shoupai) <= n_xiangting) return m;
        }
    }
}

対戦結果

今回の副露判断に評価値を適用した思考ルーチン(副露判断)と ver.0.8.8 の思考ルーチンの対戦結果は以下の通り。

ver.0.8.8鳴き考慮 副露判断 ver.0.8.8鳴き考慮 副露判断
対戦数 1,000 1,000 1,000 総局数 10,674 10,928 10,606
1位率 .244 .265 .276 和了 .199 .199 .205
2位率 .244 .251 .259 放銃率 .118 .119 .118
3位率 .249 .241 .224 立直率 .248 .249 .229
4位率 .263 .243 .241 副露率 .269 .257 .322
平均順位 2.53 2.46 2.43 平均打点 5,290 5,807 5,601

和了ver.0.8.8鳴き考慮 副露判断 和了ver.0.8.8鳴き考慮 副露判断
ドラ 49.13% 53.61% 56.12% 断幺九 22.32% 21.21% 21.25%
赤ドラ 45.07% 49,56% 47.56% 一盃口 2.74% 2.99% 3.91%
裏ドラ 23.22% 21.95% 19.46% 三色同順 1.56% 3.08% 3.96%
立直 52.52% 53.84% 50.37% 一気通貫 0.24% 0.92% 1.66%
一発 10.43% 9.66% 9.94% 七対子 3.96% 4.00% 3.50%
門前清自摸 26.52% 28.07% 26.49% 対々和 0.14% 0.46% 0.51%
翻牌 36.67% 37.51% 37.81% 混一色 0.80% 1.01% 1.38%
平和 13.50% 15.51% 14.26% 清一色 0.00% 0.09% 0.18%

平均順位は 2.43 まで上昇。効果があったと言えると思う。副露率がついに 30% を超えたが、天鳳鳳凰卓全体のの副露率は 35% 程度なので鳴き過ぎということはない。天鳳鳳凰卓では混一色の出現率は 4% を超えているので、次回はこの点を改善したい。

*1:期待値のような物であるが確率を使用していないので「評価値」としている

*2:3向聴以前も論理的には計算可能だが計算時間がかかりすぎる