和了形を求めるプログラム

麻雀の得点計算をするプログラムを書こうと思っているのだが、そのためには先に和了形を求めるプログラムを書く必要がある。というのも和了形は複数に解釈できる場合があり、その場合一番得点が高くなるパターンを選択する必要があるからだ。

和了形が複数に解釈できるパターンは以下の4つ。

  1. 七対子形とするか4面子1雀頭形とするか
  2. 順子とするか刻子とするか
  3. 雀頭をどれにするか
  4. 和了牌をどの面子に含めるか

それぞれの実例は以下の通り。

パターン1: 七対子形とするか4面子1雀頭形とするか

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

七対子形とすると断么九+七対子で3翻、4面子1雀頭形とすると断么九+二盃口で4翻。

パターン2: 順子とするか刻子とするか

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

萬子を刻子とすると三暗刻で2翻だが、順子とすれば平和+一盃口+純全帯么九で5翻になる。

パターン3: 雀頭をどれにするか

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

二萬を雀頭とすると平和+断么九+一盃口で3翻、五萬を雀頭とすると平和は消えるが断么九+一盃口三色同順で4翻になる。

パターン4: 和了牌をどの面子に含めるか

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

中のみの手だが、ツモ上がりの場合、和了牌の二萬を両面待ちの面子に入れるとちょうど30符、嵌張待ちの面子に入れると32符となり切り上げ40符となる。

これらを考慮したプログラムをJavaScriptで書いてみた。

function hule_mianzi(s, pai, i) {

    if (i == 9) return [[]];
	
    if (pai[i] == 0) return hule_mianzi(s, pai, i+1);

    var shunzi = [];
    if (i < 7 && pai[i] > 0 && pai[i+1] > 0 && pai[i+2] > 0) {
        pai[i]--; pai[i+1]--; pai[i+2]--;
        shunzi = hule_mianzi(s, pai, i);
        pai[i]++; pai[i+1]++; pai[i+2]++;
        for (var mianzi of shunzi) {
            mianzi.unshift(s+(i+1)+(i+2)+(i+3));
        }
    }
	
    var kezi = [];
    if (pai[i] >= 3) {
        pai[i] -= 3;
        kezi = hule_mianzi(s, pai, i);
        pai[i] += 3;
        for (var mianzi of kezi) {
            mianzi.unshift(s+(i+1)+(i+1)+(i+1));
        }
    }

    return shunzi.concat(kezi);
}

function hule_mianzi_all(shoupai) {

    var mianzi = [[]];

    for (var s of ['m','p','s']) {
        var new_mianzi = [];
        var sub_mianzi = hule_mianzi(s, shoupai._shouli[s], 0);
        for (var m of mianzi) {
            for (var n of sub_mianzi) {
                new_mianzi.push(m.concat(n));
            }
        }
        mianzi = new_mianzi;
    }

    var sub_mianzi_z = [];
    for (var n = 1; n <= 7; n++) {
        if (shoupai._shouli.z[n-1] == 0) continue;
        if (shoupai._shouli.z[n-1] != 3) return [];
        sub_mianzi_z.push('z'+n+n+n);
    }

    for (var i = 0; i < mianzi.length; i++) {
        mianzi[i] = mianzi[i].concat(sub_mianzi_z)
                             .concat(shoupai._fulou);
    }

    return mianzi;
}

function add_hulepai(mianzi, hulepai) {

    var regexp   = new RegExp('^(' + hulepai[0] + '.*' + hulepai[1] +')');
    var replacer = '$1' + hulepai.substr(2) + '_';

    var add_mianzi = [];
    for (var i = 0; i < mianzi.length; i++) {
        if (mianzi[i].match(/[\-\+\=]/)) continue;
        if (i > 0 && mianzi[i] == mianzi[i-1]) continue;
        var rep = mianzi[i].replace(regexp, replacer);
        if (rep == mianzi[i]) continue;
        var new_mianzi = mianzi.concat();
        new_mianzi[i] = rep;
        add_mianzi.push(new_mianzi);
    }

    return add_mianzi;
}

function hule_yiban(shoupai, rongpai) {

    var hulepai = rongpai || shoupai._zimo;

    var hule_mianzi = [];
    for (var s in shoupai._shouli) {
        var pai = shoupai._shouli[s];
        for (var n = 1; n <= pai.length; n++) {
            if (pai[n-1] < 2) continue;
            var jiangpai = s+n+n;
            pai[n-1] -= 2;
            for (var mianzi of hule_mianzi_all(shoupai)) {
                mianzi.unshift(jiangpai);
                for (var add_mianzi of add_hulepai(mianzi, hulepai)) {
                    hule_mianzi.push(add_mianzi);
                }
            }
            pai[n-1] += 2;
        }
    }

    return hule_mianzi;
}

function hule_qiduizi(shoupai, rongpai) {

    if (shoupai._fulou.length > 0) return [];

    var hulepai = rongpai || shoupai._zimo;

    var hule_mianzi = [];
    for (var s in shoupai._shouli) {
        var pai = shoupai._shouli[s];
        for (var n = 1; n <= pai.length; n++) {
            if (pai[n-1] == 0) continue;
            if (pai[n-1] == 2) {
                var p = (s+n == hulepai.substr(0,2))
                            ? s+n+n + hulepai.substr(2) + '_'
                            : s+n+n;
                hule_mianzi.push(p);
            }
            else return [];
        }
    }

    return [hule_mianzi];
}

function hule_guoshi(shoupai, rongpai) {

    var hulepai = rongpai || shoupai._zimo;

    var hule_mianzi = [];
    for (var s in shoupai._shouli) {
        var pai = shoupai._shouli[s];
        var nn = s == 'z' ? [1,2,3,4,5,6,7] : [1,9];
        for (var n of nn) {
            if (pai[n-1] == 2) {
                var p = (s+n == hulepai.substr(0,2))
                            ? s+n+n + hulepai.substr(2) + '_'
                            : s+n+n;
                hule_mianzi.unshift(p);
            }
            else if (pai[n-1] == 1) {
                var p = (s+n == hulepai.substr(0,2))
                            ? s+n + hulepai.substr(2) + '_'
                            : s+n;
                hule_mianzi.push(p);
            }
            else return [];
        }
    }

    return [hule_mianzi];
}

function hule(shoupai, rongpai) {

    if (rongpai) {
        shoupai._zimo = rongpai.substr(0,2);
        shoupai._shouli[rongpai[0]][rongpai[1]-1]++;
    }

    return [].concat(hule_yiban(shoupai, rongpai))
             .concat(hule_qiduizi(shoupai, rongpai))
             .concat(hule_guoshi(shoupai, rongpai))
}

入力となる shoupai だが形式はこんな感じ。(以下はパターン1の例)

{
    _shouli: {         // 副露していない手牌
        m: [0,2,2,2,0,0,0,0,0],   // 萬子
        p: [0,0,0,0,2,2,2,0,0],   // 筒子 
        s: [0,0,0,0,0,0,0,1,0],   // 索子
        z: [0,0,0,0,0,0,0]        // 字牌
    },
    _zimo: null,       // ツモ牌。ロンした場合はここに入れない
    _fulou: [],        // 副露牌。対面から中を鳴いた場合は 'z777=' と表記
}

期待する出力は以下の通り。"_" はツモ牌を、"-", "+", "=" は誰から得た牌かを示す。(上家: "-"、対面: "="、下家: "+")

/**** パターン1 ****/
[
 ["m22", "m33", "m44", "p55", "p66", "p77", "s88=_"],
 ["s88=_", "m234", "m234", "p567", "p567"]
]

/**** パターン2 ****/
[
 ["p99=_", "m123", "m123", "m123", "p789"],
 ["p99", "m123", "m123", "m123", "p789=_"],
 ["p99=_", "m111", "m222", "m333", "p789"],
 ["p99", "m111", "m222", "m333", "p789=_"]
]

/**** パターン3 ****/
[
 ["m22", "m3=_45", "m345", "p234", "s234"],
 ["m55", "m23=_4", "m234", "p234", "s234"]
]

/**** パターン4 ****/
[
 ["s88", "m12=_3", "m234", "p567", "z777"],
 ["s88", "m123", "m2=_34", "p567", "z777"]
]

簡単に説明すると、hule_mianzi() は各色の中で面子を探す処理(パターン2はここで処理)。

hule_mianzi_all() は4面子1雀頭形について雀頭以外の面子を探す処理。各色ごとに hule_mianzi() を呼出している。

add_hulepai()和了牌を可能性のあるすべての面子に入れる処理(パターン4はここで処理)。

hule_yiban() は4面子1雀頭形の処理。まず可能性のある雀頭を抜き出し(パターン3の処理)、hule_mianzi_all() に処理を任せた後、add_hulepai() を呼出して和了牌の位置を決めている。

hule_qiduizi()七対子形の処理。

hule_guoshi()国士無双形の処理。

hule() はメイン処理で、hule_yiban()、hule_qiduizi()、hule_guoshi() を呼出してその結果を1つにまとめている(パターン1の処理)。

これで準備はできたので、次は役判定プログラムを書く。