koba::blog

小林聡: プログラマです

ベタオリのアルゴリズム

電脳麻将 ver.0.5 では立直に対するベタオリを実装した。ベタオリするためにはまず牌の危険度を評価する必要があるが、現代麻雀技術論 - 押し引き論13.ベタオリ を参考に以下の値とした。

牌の種類 無スジ片スジスジ生牌2枚見3枚見ラス牌
字牌 3 2 1 0
一・九牌 6 3
二・八牌 8 4
三・七牌 8 5
四・五・六牌 12 8 4

牌の危険度を判定する

まず Majiang.SuanPai に上記の表にしたがい牌の危険度を判定する処理を追加する。

Majiang.SuanPai = function(hongpai) {

    /* ... ここまでは修正なし ... */

    /* 各プレイヤーごとの打牌リストを初期化する */
    this._dapai = [];
    for (var l = 0; l < 4; l++) {
        this._dapai[l] = {};
    }

    /* 各プレイヤーの立直有無を初期化する */
    this._lizhi = [false, false, false, false];
}

打牌時に打牌リストと立直有無を更新する。立直後は他者の打牌も「現物」となるので、それも打牌リストに追加する。

Majiang.SuanPai.prototype.dapai = function(data) {
    if (data.l != this._menfeng) this.diaopai(data.p);
 
    var p = data.p.substr(0,2).replace(/0/,'5');  // 赤牌を正規化
    this._dapai[data.l][p] = true;                // 打牌リストに追加

    if (data.p.match(/\*$/)) this._lizhi[data.l] = true;
                                                  // 立直有無を更新
    for (var l = 0; l < 4; l++) {
        if (this._lizhi[l]) this._dapai[l][p] = true;
                                                  // 立直後は他者の打牌も現物
    }
}

牌の危険度は Majiang.SuanPai のメソッド suan_weixian() で判定する。

Majiang.SuanPai.prototype.suan_weixian = function(p, l) {

    var rv = 12;
    var s = p[0], n = p[1]-0||5;
 
    if (this._dapai[l][s+n]) return 0;
 
    if (s == 'z')   return Math.min(this.paishu(s+n), 3);
    if (n == 1)     return this._dapai[l][s+(n+3)] ? 3 : 6;
    if (n == 9)     return this._dapai[l][s+(n-3)] ? 3 : 6;
    if (n == 2)     return this._dapai[l][s+(n+3)] ? 4 : 8;
    if (n == 8)     return this._dapai[l][s+(n-3)] ? 4 : 8;
    if (n == 3)     return this._dapai[l][s+(n+3)] ? 5 : 8;
    if (n == 7)     return this._dapai[l][s+(n-3)] ? 5 : 8;

    return    this._dapai[l][s+(n-3)] && this._dapai[l][s+(n+3)] ?  4
            : this._dapai[l][s+(n-3)] || this._dapai[l][s+(n+3)] ?  8
            :                                                      12;
}

ソースの差分は以下。

ベタオリを実装する

Majiang.Player のメソッド select_dapai() にベタオリを実装する。アルゴリズムは危険度が最も低い牌を打つというだけ。

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

    /* .... 省略 ... */

    var anquan, min = Infinity;      // 危険度の初期値を無限大とする

    /* 立直者がいる場合、以下を行う */
    if (this._lizhi.filter(function(x){return x}).length > 0) {

        /* すべての可能な打牌について危険度を判定 */
        for (var p of this.get_dapai()) {
            var weixian = 0;
            for (var l = 0; l < 4; l++) {      // すべてのプレイヤーについて
                if (! this._lizhi[l]) continue;    // 立直していない者は対象外
                var w = this._suanpai.suan_weixian(p, l);  // 危険度を判定
                if (w > weixian) weixian = w;
            }
            if (weixian < min) {    // 最も安全な牌を選択する
                min = weixian;
                anquan = p;
            }
        }
    }

    /* .... 省略 ... */
    
    if (anquan) dapai = anquan;    // 最も安全な牌を打牌に選択する
    
    /* .... 省略 ... */

    return dapai;
}

ソースの差分は以下。

ベタオリをするプレイヤーとオリないプレーヤー3人の対戦結果は以下の通り。

対戦数 1,000総局数 9,460
1位率 .191和了 .159
2位率 .311放銃率 .031
3位率 .363立直率 .255
4位率 .135副露率 .000
平均順位 2.44平均打点 6,803

対戦の様子を見てみると、ベタオリさえすれば現物が足りなくなる場面はほとんどないようである。つまりベタオリするだけなら牌の危険度の精度はこれくらいで充分*1。放銃率 3.1% はほぼ放銃しないと言えるレベルだが、オリすぎて和了率が 15.9% まで落ちてしまった。その結果、トップ率も落ちてしまい、平均順位としては大きな改善とならなかった。何よりオリ過ぎでつまらない麻雀になっている。

回し打ちを仮実装する

そこで少しだけ押してみることにした。具体的には、

とした。

対戦結果は以下の通り。

対戦数 1,000総局数 9,425
1位率 .257和了 .198
2位率 .342放銃率 .055
3位率 .278立直率 .361
4位率 .123副露率 .000
平均順位 2.27平均打点 7,137

平均順位が 2.27 に上昇。今までで一番効果の高い改善となった*3。放銃率も 5.5% であれば問題ない。押し引きについてはいずれまた改善するつもりであるが、とりあえずこれを ver.0.5 のアルゴリズムに採用した。

*1:回し打ちをするなら壁の情報などを加えて精度を上げた方がよさそう

*2:つまり無スジは押さない

*3:やはり麻雀は押し引きが一番重要ということなのだろう