ドラ
から何を切るか?*1
候補となるのは、打 (待ち 16枚)、打 (待ち 16枚)。麻雀の副露判断アルゴリズム(4) - koba::blog までのアルゴリズムでは がポンできることを重視し だが、麻雀の打牌選択アルゴリズム(7) - koba::blog のアルゴリズムは2向聴以降は鳴きを考慮しないため を選択してしまう。
今回は2向聴以降も鳴きを考慮できるよう改良する。
評価値の検討
上記牌姿から もしくは を打牌した場合、聴牌形は以下のいずれかになる。
No | 聴牌形 | 待ち | 枚数 | 打点 | 評価値 |
---|---|---|---|---|---|
1 | 4 | 4000 | 1,333.33 | ||
2 | 4 | 7900 | 2,633.33 | ||
3 | 4 | 4000 | 2,991.67 | ||
1 | 7900 | ||||
3 | 4000 | ||||
4 | 4 | 2700 | 2,008.33 | ||
1 | 5200 | ||||
3 | 2700 | ||||
5 | 4 | 5200 | 3,700.00 | ||
1 | 8000 | ||||
3 | 5200 |
打 、 からそれぞれ上記テンパイに至る枚数は、
打牌 | 各聴牌形への待ち牌 | 評価値 | ||||
---|---|---|---|---|---|---|
No.1 | No.2 | No.3 | No.4 | No.5 | ||
7枚 | 1枚 | 4枚 | 2枚 | 2枚 | 490.97 | |
7枚 | 1枚 | 8枚 | 498.61 |
となり、前回までのアルゴリズムでは打 が最高の評価値となる。しかし、打 の場合は ポンでも以下の聴牌形をとることができる。
No | 聴牌形 | 待ち | 枚数 | 打点 | 評価値 |
---|---|---|---|---|---|
6 | 4 | 1100 | 808.33 | ||
1 | 2000 | ||||
3 | 1100 |
これを含めると評価値は
打牌 | 各聴牌形への待ち牌 | 評価値 | |||||
---|---|---|---|---|---|---|---|
No.1 | No.2 | No.3 | No.4 | No.5 | No.6 | ||
7枚 | 1枚 | 4枚 | 2枚 | 2枚 | 2×3枚 | 588.89 | |
7枚 | 1枚 | 8枚 | 498.61 |
となり*2、打 が有利となる。
プログラム実装
まず。副露を評価するメソッド eval_fulou()
を追加する。
Majiang.Player.prototype.eval_fulou = function(shoupai, p, paishu) { /* 現在の向聴数を求める */ var n_xiangting = Majiang.Util.xiangting(shoupai); if (n_xiangting <= 0) return; // テンパイ以降は評価しない /* ポンした場合の評価値を求める */ var r = 0, peng_max = 0; for (var m of get_peng_mianzi(shoupai, p+'+')) { var new_shoupai = shoupai.clone(); // 手牌を複製する new_shoupai.fulou(m); // 実際に鳴いてみる if (Majiang.Util.xiangting(new_shoupai) >= n_xiangting) continue; // 向聴数が戻る場合は評価対象外 var ev = this.eval_shoupai(new_shoupai, paishu); // 鳴いた手牌の評価値を求める if (ev > peng_max) peng_max = ev; // peng_max を最高の評価値とする } /* チーした場合の評価値を求める */ var chi_max = 0; for (var m of get_chi_mianzi(shoupai, p+'-')) { var new_shoupai = shoupai.clone(); // 手牌を複製する new_shoupai.fulou(m); // 実際に鳴いてみる if (Majiang.Util.xiangting(new_shoupai) >= n_xiangting) continue; // 向聴数が戻る場合は評価対象外 var ev = this.eval_shoupai(new_shoupai, paishu); // 鳴いた手牌の評価値を求める if (ev > chi_max) chi_max = ev; // chi_max を最高の評価値とする } /* ポンの評価値が高い場合: 評価値 = ポンの評価値 × 3 チーの評価値が高い場合: 評価値 = ポンの評価値 × 2 + チーの評価値 */ return (peng_max > chi_max) ? peng_max * 3 : peng_max * 2 + chi_max; }
1向聴および2向聴の場合に、鳴いた場合の評価値を加える。シャンテン戻しの検討中(dapai
にシャンテン戻しの打牌が設定されている)は副露の評価はしない。*3
Majiang.Player.prototype.eval_shoupai = function(shoupai, paishu, dapai) { /* ………… */ if (n_xiangting == -1) { /* ………… */ } else if (shoupai._zimo) { /* ………… */ } else if (n_xiangting < 3) { var r = 0; for (var p of add_hongpai(Majiang.Util.tingpai(shoupai))) { /* ………… */ var ev = this.eval_shoupai(new_shoupai, paishu, dapai); if (! dapai && n_xiangting > 0) // シャンテン戻しの検討中および // テンパイ時は鳴きを考慮しない ev += this.eval_fulou(shoupai, p, paishu); // 副露を評価する /* ………… */ } rv = r / width[n_xiangting]; } else { /* ………… */ } /* ………… */ }
これで鳴きも評価できるようになったが、またも計算速度が遅くなってしまった。次の2つの方法で速度向上をはかる。
- 和了打点キャッシュし、同じ牌姿を二度計算しないようにする
- 2向聴では役がある鳴きのみ考慮するようにする
評価値はすでにキャッシュしているがこれに加えて和了打点もキャッシュするようにした。評価値は残牌数もパラメータとなるので一手毎にキャッシュをクリアしているが、和了打点は牌姿だけで求められる*4ので、一局毎にキャッシュをクリアすればよい。
Majiang.Player.prototype.qipai = function(data) { /* ………… */ this._defen_cache = {}; // 配牌時にキャッシュをクリアする } /* ………… */ Majiang.Player.prototype.get_defen = function(shoupai) { /* すでに和了打点計算済みの牌姿の場合、それを返す */ var paistr = shoupai.toString(); if (this._defen_cache[paistr]) return this._defen_cache[paistr]; /* ………… */ this._defen_cache[paistr] = hule.defen; // 和了打点をキャッシュする return hule.defen; }
鳴いた場合の評価値は、そのほとんどが0点となる(役なしテンパイとなる場合が多い)ため、2向聴の場合は「役がある場合」のみ評価値を計算する。役の有無は 麻雀の副露判断アルゴリズム(2) - koba::blog で作成した向聴数計算ルーチンで判断するが、このルーチンは喰い三色などは考慮していないため、2向聴からの喰い三色は評価されなくなってしまう(が速度向上のためやむなし)。
まず副露を評価するメソッド eval_fulou()
に向聴数計算ルーチンを指定できるようにする。
Majiang.Player.prototype.eval_fulou = function(shoupai, p, paishu, xiangting) { // パラメータ xiangting を追加し向聴数 // 計算ルーチンを変更できるようにした xiangting = xiangting || Majiang.Util.xiangting; // 指定されない場合汎用の向聴数計算 // ルーチンを用いる var n_xiangting = xiangting(shoupai); // 指定された向聴数計算ルーチン // を使用 if (n_xiangting <= 0) return; var r = 0, peng_max = 0; for (var m of get_peng_mianzi(shoupai, p+'+')) { var new_shoupai = shoupai.clone(); new_shoupai.fulou(m); if (xiangting(new_shoupai) >= n_xiangting) continue; // 指定された向聴数計算ルーチン // を使用 var ev = this.eval_shoupai(new_shoupai, paishu); if (ev > peng_max) peng_max = ev; } var chi_max = 0; for (var m of get_chi_mianzi(shoupai, p+'-')) { var new_shoupai = shoupai.clone(); new_shoupai.fulou(m); if (xiangting(new_shoupai) >= n_xiangting) continue; // 指定された向聴数計算ルーチン // を使用 var ev = this.eval_shoupai(new_shoupai, paishu); if (ev > chi_max) chi_max = ev; } return (peng_max > chi_max) ? peng_max * 3 : peng_max * 2 + chi_max; }
eval_shoupai()
では2向聴の場合、役を考慮した向聴数計算ルーチンを使用する。
Majiang.Player.prototype.eval_shoupai = function(shoupai, paishu, dapai) { var self = this; /* ………… */ if (n_xiangting == -1) { /* ………… */ } else if (shoupai._zimo) { /* ………… */ } else if (n_xiangting < 3) { var r = 0; for (var p of add_hongpai(Majiang.Util.tingpai(shoupai))) { /* ………… */ var ev = this.eval_shoupai(new_shoupai, paishu, dapai); if (! dapai) { if (n_xiangting > 1) ev += this.eval_fulou(shoupai, p, paishu, function(s){return self.xiangting(s)}); // 役を考慮した向聴数計算ルーチンを指定 else if (n_xiangting > 0) ev += this.eval_fulou(shoupai, p, paishu); } /* ………… */ } rv = r / width[n_xiangting]; } else { /* ………… */ } /* ………… */ }
対戦結果
例によって今回の鳴きを考慮した思考ルーチン(鳴き考慮)と ver.0.8.8 の思考ルーチンを対戦させてみた。
ver.0.8.8 | 向聴戻し | 鳴き考慮 | ver.0.8.8 | 向聴戻し | 鳴き考慮 | ||
---|---|---|---|---|---|---|---|
対戦数 | 1,000 | 1,000 | 1,000 | 総局数 | 10,674 | 10,609 | 10,928 |
1位率 | .244 | .232 | .265 | 和了率 | .199 | .190 | .199 |
2位率 | .244 | .272 | .251 | 放銃率 | .118 | .118 | .119 |
3位率 | .249 | .256 | .241 | 立直率 | .248 | .246 | .249 |
4位率 | .263 | .240 | .243 | 副露率 | .269 | .253 | .257 |
平均順位 | 2.53 | 2.50 | 2.46 | 平均打点 | 5,290 | 5,764 | 5,807 |
和了役 | ver.0.8.8 | 向聴戻し | 鳴き考慮 | 和了役 | ver.0.8.8 | 向聴戻し | 鳴き考慮 | |
---|---|---|---|---|---|---|---|---|
ドラ | 49.13% | 55.76% | 53.61% | 断幺九 | 22.32% | 20.04% | 21.21% | |
赤ドラ | 45.07% | 48.64% | 49,56% | 一盃口 | 2.74% | 2.82% | 2.99% | |
裏ドラ | 23.22% | 23.40% | 21.95% | 三色同順 | 1.56% | 2.13% | 3.08% | |
立直 | 52.52% | 53.59% | 53.84% | 一気通貫 | 0.24% | 1.09% | 0.92% | |
一発 | 10.43% | 10.44% | 9.66% | 七対子 | 3.96% | 4.06% | 4.00% | |
門前清自摸和 | 26.52% | 28.40% | 28.07% | 対々和 | 0.14% | 0.30% | 0.46% | |
翻牌 | 36.67% | 38.89% | 37.51% | 混一色 | 0.80% | 1.53% | 1.01% | |
平和 | 13.50% | 14.65% | 15.51% | 清一色 | 0.00% | 0.10% | 0.09% |
予想していたことだが、やはり成績に大きな影響はない模様。次回は副露判断に評価値を使用することを試みる。
*1:麻雀 傑作「何切る」300選 での正解は
*2:ポンは3人からできるため、枚数は3倍に評価する
*3:鳴くためにシャンテン戻しした訳ではなかろう
*4:牌姿以外の条件は 麻雀の打牌選択アルゴリズム(4) - koba::blog で述べたように単純化している