koba::blog

小林聡: プログラマです

麻雀の和了点を計算するプログラム(最終回)

で説明してきた和了点を計算するプログラムも今日で最終回。いよいよ最終目的の点数計算です。

まずは麻雀のルールをおさらい。符と翻数を求めた後は以下の手順で得点を求める。

基本点の算出

  1. 符を4倍する (場ゾロ)
  2. 翻数分だけ2倍する

で得られた点が基本点になるというのが基本ルールだが、このままでは点数が大きくなりすぎる(いわゆる青天井)ので以下の制約がある。

  • 役満の基本点は 8,000点
  • 13翻以上の場合は 8,000点 (数え役満)
  • 11翻以上の場合は 6,000点 (三倍満)
  • 8翻以上の場合は 4,000点 (倍満)
  • 6翻以上の場合は 3,000点 (跳満)
  • 2,000点を超える場合は 2,000点で切り捨て (満貫)

負担額の決定

ロン和了の場合
放銃者が1人で負担する。
支払う得点は、親の和了の場合 基本点 x 6、子の和了の場合、基本点 x 4
ツモ和了の場合
3人で分担する
支払う得点は、親の和了の場合、基本点 x 2、子の和了の場合、親の負担: 基本点 x 2、子の負担: 基本点 x 1 とする。

負担額の100点未満は切り上げる。

供託の扱い

  • 立直棒は和了者の収入となる。
  • 積み棒は、ロン和了の場合放銃者が負担(本数 x 300点)、ツモ和了の場合3人で分担(各自 本数 x 100点)する。

ダブロンの場合は供託収入は頭ハネとする。

役満パオの扱い

ロン和了の場合放銃者と折半、ツモ和了の場合1人払いというのが基本だが、役満の複合があった場合(大三元字一色など)はパオ対象者は成立させた役満にのみ責任を持つのが一般的なルールと思う*1

電脳麻将 でもパオ対象者は成立させた役満にのみ責任を持つこととした*2ので、以下のルールとなる。

  • パオ対象者が全役満に責任がある場合
    • ロン和了の場合
      • 負担額は放銃者と折半、供託は放銃者が負担
    • ツモ和了の場合
      • 供託も含めパオ対象者の全額負担
  • パオ対象者に責任のない役満がある場合
    • ロン和了の場合
      • パオ対象の役満の負担額は放銃者と折半、それ以外の役満と供託は放銃者が負担
    • ツモ和了の場合
      • パオ対象の役満はパオ対象者が負担、それ以外の役満と供託は通常ルールに従う

上記の手順に従った和了点計算は、関数 Majiang.Util.hule() の後半部分で行っている。

Majiang.Util.hule = function(shoupai, rongpai, param) {

    var max = {
        hupai:      null,
        fu:         0,
        fanshu:     0,
        damanguan:  0,
        defen:      0,
        fenpei:     [ 0, 0, 0, 0 ]
    };
    
    var pre_hupai  = get_pre_hupai(param.hupai);
    var post_hupai = get_post_hupai(
                        shoupai.toString(), param.baopai, param.fubaopai);
    
    for (var mianzi of hule_mianzi(shoupai, rongpai)) {
    
        var hudi  = get_hudi(mianzi, param.zhuangfeng, param.menfeng);
        var hupai = get_hupai(mianzi, hudi, pre_hupai);
        
        if (hupai.length == 0) continue;

        /**** ここからが点数計算 ****/
        
        var fu = hudi.fu;
        var fanshu = 0, defen = 0, damanguan = 0;
 
        var baojia2, defen2 = 0;

        if (hupai[0].fanshu[0] == '*') {       // 役満の場合
            for (var h of hupai) {    // 複合する役満すべてについて以下を行う
                damanguan += h.fanshu.match(/\*/g).length; // 役満複合数を加算
                if (h.baojia) {       // パオの場合
                    /* -, +, = の記号からパオ対象者を特定する */
                    baojia2 = h.baojia == '+' ? (param.menfeng + 1) % 4
                            : h.baojia == '=' ? (param.menfeng + 2) % 4
                            : h.baojia == '-' ? (param.menfeng + 3) % 4
                            : -1;
                    /* パオ対象の基本点を求める */
                    defen2  = 8000 * h.fanshu.match(/\*/g).length;
                }
            }
            /* パオを含む全体の基本点を求める */
            defen = 8000 * damanguan;
        }
        else {                                 // 役満以外の場合
            hupai = hupai.concat(post_hupai);  // 懸賞役を加える
            for (var h of hupai) { fanshu += h.fanshu }  // 翻数を決定する
            /* 基本点を求める */
            if      (fanshu >= 13) defen = 8000;  // 役満
            else if (fanshu >= 11) defen = 6000;  // 三倍満
            else if (fanshu >=  8) defen = 4000;  // 倍満
            else if (fanshu >=  6) defen = 3000;  // 跳満
            else {
                defen = fu * 2 * 2;               // 符を4倍する (場ゾロ)
                for (var i = 0; i < fanshu; i++) { defen *= 2 }
                                                  // 翻数分だけ2倍する
                if (defen >= 2000) defen = 2000;  // 2000点を上限とする(満貫)
            }
        }
        
        var fenpei = [ 0, 0, 0, 0 ];    // 収入と負担額を初期化する
 
        if (defen2 > 0) {               // パオがあった場合、先に精算する
            if (rongpai) defen2 = defen2 / 2;  // ロン和了の場合は放銃者と折半
            defen  = defen - defen2;           // 基本点からパオ分を減算
            defen2 = defen2 * (param.menfeng == 0 ? 6 : 4);
                                               // パオ分の負担額を求める
            fenpei[param.menfeng] =  defen2;   // 和了者の収入を加算
            fenpei[baojia2]       = -defen2;   // パオ対象の負担額を減算
        }
 
        var changbang = param.jicun.changbang;    // 積み棒
        var lizhibang = param.jicun.lizhibang;    // 立直棒
        
        if (rongpai || defen == 0) {    // ロン和了もしくはパオ1人払いの場合
            /* -, +, = の記号から放銃者を特定する */
            var baojia = defen == 0        ? baojia2  // パオ1人払いは放銃者扱い
                       : rongpai[2] == '+' ? (param.menfeng + 1) % 4
                       : rongpai[2] == '=' ? (param.menfeng + 2) % 4
                       : rongpai[2] == '-' ? (param.menfeng + 3) % 4
                       : -1;
            /* 負担額を求める(100点未満切り上げ) */
            defen = Math.ceil(defen * (param.menfeng == 0 ? 6 : 4) / 100) * 100;
            fenpei[param.menfeng] +=  defen + changbang * 300 + lizhibang * 1000;
                                             // 和了者の収入を加算(供託含む)
            fenpei[baojia]        += -defen - changbang * 300;
                                             // 放銃者の負担額を減算(供託含む)
        }
        else {                     // ツモ和了の場合
            var zhuangjia = Math.ceil(defen * 2 / 100) * 100;  // 親の負担額
            var sanjia    = Math.ceil(defen     / 100) * 100;  // 子の負担額

            if (param.menfeng == 0) {  // 親の和了の場合
                defen = zhuangjia * 3;       // 和了打点は 親の負担額 x 3
                for (var l = 0; l < 4; l++) {
                    if (l == param.menfeng)
                        fenpei[l] += defen + changbang * 300 + lizhibang * 1000;
                                             // 和了者の収入を加算(供託含む)
                    else
                        fenpei[l] += -zhuangjia - changbang * 100;
                                             // 負担者の負担額を減算(供託含む)
                }
            }
            else {                 // 子の和了の場合
                defen = zhuangjia + sanjia * 2;
                                       // 和了打点は 親の負担額 + 子の負担額 x 2
                for (var l = 0; l < 4; l++) {
                    if (l == param.menfeng)
                        fenpei[l] += defen      + changbang * 300
                                               + lizhibang * 1000;
                                             // 和了者の収入を加算(供託含む)
                    else if (l == 0)
                        fenpei[l] += -zhuangjia - changbang * 100;
                                             // 親の負担額を減算(供託含む)
                    else
                        fenpei[l] += -sanjia    - changbang * 100;
                                             // 子の負担額を減算(供託含む)
                }
            }
        }
        
        /* 得られた和了点の最大値を解とする */
        if (defen + defen2 > max.defen) {
            max = {
                hupai:      hupai,           // 和了役一覧
                fu:         fu,              // 符
                fanshu:     fanshu,          // 翻数
                damanguan:  damanguan,       // 役満複合数
                defen:      defen + defen2,  // 和了打点
                fenpei:     fenpei           // 局収支
            };
        }
    }
    
    return max;
}

和了点計算全体のソースコードはこちら

上記ソースを使ったデモプログラムも作りました。

*1:天鳳では役満が複合した場合でもパオ対象者は全役満に責任を負わされているが、バグじゃないのか?

*2:そのうち天鳳ルールにあわせる可能性あり