和了形を求めるプログラム(一般形)

和了形を求めるプログラム(特殊形) - koba::blog の続き。

一般形(4面子1雀頭)の和了形を求めるメイン関数は hule_mianzi_yiban()。処理の流れは以下の通り。

  1. 2枚以上ある牌を雀頭候補として抜き取る
  2. 残りの牌で4面子構成できるか調べるために mianzi_all() を呼出す
  3. 得られた4面子に雀頭を差し込んで和了形にする
  4. add_hulepai() を呼出して和了形の面子に和了牌のマークをつける

の繰り返し。

function hule_mianzi_yiban(shoupai, hulepai) {

    var hule_mianzi = [];
    
    for (var s in shoupai._bingpai) {
        var bingpai = shoupai._bingpai[s];
        for (var n = 1; n < bingpai.length; n++) {
            if (bingpai[n] < 2) continue;
            bingpai[n] -= 2;       // 2枚以上ある牌を雀頭候補として抜き取る
            var jiangpai = s+n+n;
            for (var mm of mianzi_all(shoupai)) { // 残りの牌で面子構成を求める
                mm.unshift(jiangpai);             // 面子構成に雀頭を差し込む
                if (mm.length != 5) continue;
                hule_mianzi = hule_mianzi.concat(add_hulepai(mm, hulepai));
                                                  // 和了牌のマークをつける
            }
            bingpai[n] += 2;
        }
    }
    
    return hule_mianzi;
}

結果の和了形は複数得られる場合がある。それは、(1) 雀頭候補が複数ある場合、(2) 面子構成が複数ある場合、(3) 和了牌のマークをつける箇所が複数ある場合、である。

(1) 雀頭候補が複数ある場合

二萬二萬三萬四萬四萬五萬五萬二筒三筒四筒二索三索四索三萬

二萬と五萬が雀頭になり得るので、返り値は、

 [ [ 'm22', 'm3_!45', 'm345', 'p234', 's234' ],
   [ 'm55', 'm23_!4', 'm234', 'p234', 's234' ] ]

となる。

(2) 面子の構成が複数ある場合

一萬一萬一萬二萬二萬二萬三萬三萬三萬八筒九筒九筒九筒七筒

萬子が順子とも刻子ともとれるので、返り値は、

 [ [ 'p99', 'm123', 'm123', 'm123', 'p7_!89' ],
   [ 'p99', 'm111', 'm222', 'm333', 'p7_!89' ] ]

となる。

(3) 和了牌のマークをつける箇所が複数ある場合

一萬二萬三萬三萬四萬五筒六筒七筒八索八索中中中二萬

和了牌の二萬が嵌張待ちとも両面待ちともとれるので、返り値は、

 [ [ 's88', 'm12_!3', 'm234', 'p567', 'z777' ],
   [ 's88', 'm123', 'm2_!34', 'p567', 'z777' ] ]

となる。

面子構成を求める

面子構成を求める関数は mianzi_all()。本関数は色(萬子、筒子、索子)ごとに mianzi() を呼出し、結果をまとめているだけ。

function mianzi_all(shoupai) {

    var all_mianzi = [[]];
    
    /* 萬子、筒子、索子の副露していない牌から面子を探す */
    for (var s of ['m','p','s']) {
        var new_mianzi = [];
        var sub_mianzi = mianzi(s, shoupai._bingpai[s], 1);
                                                 // 色ごとに mianzi() を呼出す
        for (var mm of all_mianzi) {
            for (var nn of sub_mianzi) {
                new_mianzi.push(mm.concat(nn));  // 結果をマージする
            }
        }
        all_mianzi = new_mianzi;
    }
    
    /* 字牌の面子は刻子しかあり得ないので自前で処理する */
    var sub_mianzi_z = [];
    for (var n = 1; n <= 7; n++) {
        if (shoupai._bingpai.z[n] == 0) continue;
        if (shoupai._bingpai.z[n] != 3) return [];
        sub_mianzi_z.push('z'+n+n+n);
    }
    
    /* 副露済みの面子を後方に追加する */
    var fulou = shoupai._fulou.map(function(m){return m.replace(/0/g ,'5')});
    for (var i = 0; i < all_mianzi.length; i++) {
        all_mianzi[i] = all_mianzi[i].concat(sub_mianzi_z)
                                     .concat(fulou);
    }
    
    return all_mianzi;
}

面子を抜き出す

実際に面子を抜き出す関数 mianzi()向聴数を求めた際の関数と似ているが、以下が異なる*1

  1. 搭子を残す必要がない(搭子が残った場合は和了形ではない)
  2. 面子の数ではなく、とりうる面子の組合わせすべてを返す必要がある
function mianzi(s, bingpai, n) {

    if (n > 9) return [[]];
    
    /* 面子を抜き取り終わったら、次の位置に進む */
    if (bingpai[n] == 0) return mianzi(s, bingpai, n+1);
    
    /* 順子を抜き取る */
    var shunzi = [];
    if (n <= 7 && bingpai[n] > 0 && bingpai[n+1] > 0 && bingpai[n+2] > 0) {
        bingpai[n]--; bingpai[n+1]--; bingpai[n+2]--;
        shunzi = mianzi(s, bingpai, n);  // 抜き取ったら同じ位置でもう一度試行
        bingpai[n]++; bingpai[n+1]++; bingpai[n+2]++;
        for (var s_mianzi of shunzi) {
            s_mianzi.unshift(s+(n)+(n+1)+(n+2));
        }
    }
    
    /* 刻子を抜き取る */
    var kezi = [];
    if (bingpai[n] >= 3) {
        bingpai[n] -= 3;
        kezi = mianzi(s, bingpai, n);    // 抜き取ったら同じ位置でもう一度試行
        bingpai[n] += 3;
        for (var k_mianzi of kezi) {
            k_mianzi.unshift(s+n+n+n);
        }
    }
    
    return shunzi.concat(kezi);  // 順子と刻子の結果をマージして返す
}

入力が 一萬一萬一萬二萬二萬二萬三萬三萬三萬 の場合、mianzi()

 [ [ 'm123', 'm123', 'm123' ],
   [ 'm111', 'm222', 'm333' ] ]

を返す。

和了牌のマークをつける

add_hulepai()和了形から和了牌を探してマークをつける関数。

function add_hulepai(hule_mianzi, p) {

    var regexp   = new RegExp('^(' + p[0] + '.*' + (p[1] || '5') +')');
    var replacer = '$1' + p[2] + '!';
    
    var new_mianzi = [];
    
    for (var i = 0; i < hule_mianzi.length; i++) {
        if (hule_mianzi[i].match(/[\-\+\=]/)) continue;
        if (i > 0 && hule_mianzi[i] == hule_mianzi[i-1]) continue;
        var m = hule_mianzi[i].replace(regexp, replacer);
        if (m == hule_mianzi[i]) continue;
        var tmp_mianzi = hule_mianzi.concat();
        tmp_mianzi[i] = m;
        new_mianzi.push(tmp_mianzi);
    }
    
    return new_mianzi;
}

和了形の複数の面子に和了牌を見つけた場合は、そのすべてにマークをつける。

入力が、

 hule_mianzi: [ 's88', 'm123', 'm234', 'p567', 'z777' ],
 p:           'm2_'

の場合、'm2' が2面子にあるので返り値はそのそれぞれにマークをつけた

 [ [ 's88', 'm12_!3', 'm234', 'p567', 'z777' ],
   [ 's88', 'm123', 'm2_!34', 'p567', 'z777' ] ]

となる。

これで和了形は求められたので、次は符計算。

*1:関数名は同じだがスコープが違うので別物