koba::blog

小林聡: プログラマです

麻雀の役を判定するプログラム(再)

以前 麻雀の役を判定するプログラム - koba::blog でも一度扱っているが、そのときは正規表現で役を判定していた。今回は 麻雀の符を求めるプログラム - koba::blog で面子構成を求めているのでそれを使って役を判定していく。

電脳麻将和了役判定関数は get_hupai(mianzi, hudi, pre_hupai)

入力は以下の通り。

和了役判定関数をのぞいた全体の構造は以下の通り。

function get_hupai(mianzi, hudi, pre_hupai) {

    /**** 役を判定する関数は個別に説明する ****/

    /* 役満の初期値を設定する。状況役に役満(天和、地和)が含まれている場合は
       それを設定、ない場合は空配列で初期化。                               */
    var damanguan = (pre_hupai.length > 0 && pre_hupai[0].fanshu[0] == '*')
                        ? pre_hupai : [];

    /* 判定できた役満を追加していく */
    damanguan = damanguan
                .concat(guoshiwushuang())
                .concat(sianke())
                .concat(dasanyuan())
                .concat(sixihu())
                .concat(ziyise())
                .concat(lvyise())
                .concat(qinglaotou())
                .concat(sigangzi())
                .concat(jiulianbaodeng());

    if (damanguan.length > 0) return damanguan;  // 役満がある場合は処理終了
    else return pre_hupai                        // 役満がない場合は状況役に
                .concat(menqianqing())           // 役を追加していく
                .concat(fanpai())
                .concat(pinghu())
                .concat(duanyaojiu())
                .concat(yibeikou())
                .concat(sansetongshun())
                .concat(yiqitongguan())
                .concat(hunquandaiyaojiu())
                .concat(qiduizi())
                .concat(duiduihu())
                .concat(sananke())
                .concat(sangangzi())
                .concat(sansetongke())
                .concat(hunlaotou())
                .concat(xiaosanyuan())
                .concat(hunyise())
                .concat(chunquandaiyaojiu())
                .concat(erbeikou())
                .concat(qingyise());
}

和了役判定関数は役ごとにあるので1つずつ説明する。

門前清自摸

    function menqianqing() {
        if (hudi.menqian && hudi.zimo)
                return [{ name: '門前清自摸和', fanshu: 1 }];
        return [];
    }

門前でツモ和了の場合は門前清自摸和。

翻牌

    function fanpai() {
        var feng_hanzi = ['東','南','西','北'];
        var fanpai_all = [];
        if (hudi.kezi.z[hudi.zhuangfeng+1])
                fanpai_all.push({ name: '場風 ' + feng_hanzi[hudi.zhuangfeng],
                                  fanshu: 1 });
        if (hudi.kezi.z[hudi.menfeng+1])
                fanpai_all.push({ name: '自風 ' + feng_hanzi[hudi.menfeng],
                                  fanshu: 1 });
        if (hudi.kezi.z[5]) fanpai_all.push({ name: '翻牌 白', fanshu: 1 });
        if (hudi.kezi.z[6]) fanpai_all.push({ name: '翻牌 發', fanshu: 1 });
        if (hudi.kezi.z[7]) fanpai_all.push({ name: '翻牌 中', fanshu: 1 });
        return fanpai_all;
    }

翻牌は牌の種類を示したいので少し面倒。調査済みの刻子構成に翻牌が含まれていれば追加していく。

平和

    function pinghu() {
        if (hudi.pinghu)        return [{ name: '平和', fanshu: 1 }];
        return [];
    }

平和はすでに判定済みなのでそれをそのまま使う。

断幺九

    function duanyaojiu() {
        if (hudi.n_yaojiu == 0) return [{ name: '断幺九', fanshu: 1 }];
        return [];
    }

幺九牌を含む面子がなければ断幺九

一盃口

    function yibeikou() {
        if (! hudi.menqian)     return [];
        var beikou = 0;
        for (var s in hudi.shunzi) {
            for (var m in hudi.shunzi[s]) {
                if (hudi.shunzi[s][m] > 3) beikou++;
                if (hudi.shunzi[s][m] > 1) beikou++;
            }
        }
        if (beikou == 1)        return [{ name: '一盃口', fanshu: 1 }];
        return [];
    }

一盃口はちょっと複雑。調査済みの順子構成に同一の面子がいくつあるかで判定し、2の倍数ごとに「盃口数」をカウントアップしている。

 { m: { "456": 2 }, p: { "789": 1 }, s: { } }

上記の面子構成の場合は盃口数は 1。役の制約条件があり、門前でないと役とはならないし、盃口数が 2 の場合は二盃口になるので要注意。

三色同順

    function sansetongshun() {
        var shunzi = hudi.shunzi;
        for (var m in shunzi.m) {
            if (shunzi.p[m] && shunzi.s[m])
                return [{ name: '三色同順', fanshu: (hudi.menqian ? 2 : 1) }];
        }
        return [];
    }

萬子にある順子の並びと同一の並びが筒子と索子にもあれば三色同順。例えば

 { m: { "123": 1, "456": 1 }, p: { "456": 1 }, s: { "456": 1 } }

の場合など。喰い下がり役なので門前か否かで翻数を変えている。

一気通貫

    function yiqitongguan() {
        var shunzi = hudi.shunzi;
        for (var s in shunzi) {
            if (shunzi[s][123] && shunzi[s][456] && shunzi[s][789])
                return [{ name: '一気通貫', fanshu: (hudi.menqian ? 2 : 1) }];
        }
        return [];
    }

同一色に 123, 456, 789 の順子があれば一気通貫。例えば

 { m: { }, p: { "123": 1, "456": 1, "789": 1 }, s: { } }

の場合など。これも喰い下がり役。

全帯幺

    function hunquandaiyaojiu() {
        if (hudi.n_yaojiu == 5 && hudi.n_shunzi > 0 && hudi.n_zipai > 0)
                return [{ name: '混全帯幺九', fanshu: (hudi.menqian ? 2 : 1) }];
        return [];
    }

幺九牌を含む面子が5つあり、順子と字牌面子もあれば混全帯幺九。順子がない場合は混老頭に、字牌がない場合は純全帯幺九になり、複合しない点に注意。

七対子

    function qiduizi() {
        if (mianzi.length == 7)     return [{ name: '七対子', fanshu: 2 }];
        return [];
    }

面子が7つの場合は七対子

対々和

    function duiduihu() {
        if (hudi.n_kezi == 4)       return [{ name: '対々和', fanshu: 2 }];
        return [];
    }

刻子が4つの場合は対々和

三暗刻

    function sananke() {
        if (hudi.n_ankezi == 3)     return [{ name: '三暗刻', fanshu: 2 }];
        return [];
    }

刻子が3つの場合は三暗刻

三槓子

    function sangangzi() {
        if (hudi.n_gangzi == 3)     return [{ name: '三槓子', fanshu: 2 }];
        return [];
    }

槓子が3つの場合は三槓子

三色同刻

    function sansetongke() {
        var kezi = hudi.kezi;
        for (var n = 1; n <= 9; n++) {
            if (kezi.m[n] + kezi.p[n] + kezi.s[n] == 3)
                                    return [{ name: '三色同刻', fanshu: 2 }];
        }
        return [];
    }

3つの色に同一の数牌の刻子があれば三色同刻

混老頭

    function hunlaotou() {
        if (hudi.n_yaojiu == mianzi.length
            && hudi.n_shunzi == 0 && hudi.n_zipai > 0)
                                    return [{ name: '混老頭', fanshu: 2 }];
        return [];
    }

全面子が幺九牌を含み、順子がなく、字牌面子があれば混老頭混老頭七対子形があり得るところに注意。

小三元

    function xiaosanyuan() {
        if (hudi.kezi.z[5] + hudi.kezi.z[6] + hudi.kezi.z[7] == 2
            && mianzi[0].match(/^z[567]/))
                                    return [{ name: '小三元', fanshu: 2 }];
        return [];
    }

三元牌刻子が2つあり、雀頭三元牌なら小三元

混一色

    function hunyise() {
        for (var s of ['m','p','s']) {
            var yise = new RegExp('^[z' + s + '].*$');
            if (mianzi.filter(function(m){return m.match(yise)}).length
                        == mianzi.length
                &&  hudi.n_zipai > 0)
                    return [{ name: '混一色', fanshu: (hudi.menqian ? 3 : 2) }];
        }
        return [];
    }

一色手はパターンマッチングが必要。色ごとに全面子がその色か字牌にマッチするか調べ、マッチしてかつ字牌面子がある場合、混一色

純全帯幺

    function chunquandaiyaojiu() {
        if (hudi.n_yaojiu == 5 && hudi.n_shunzi > 0 && hudi.n_zipai == 0)
                return [{ name: '純全帯幺九', fanshu: (hudi.menqian ? 3 : 2) }];
        return [];
    }

幺九牌を含む面子が5つで、順子があり、字牌面子がなければ純全帯幺九。

二盃口

    function erbeikou() {
        if (! hudi.menqian)     return [];
        var beikou = 0;
        for (var s in hudi.shunzi) {
            for (var m in hudi.shunzi[s]) {
                if (hudi.shunzi[s][m] > 3) beikou++;
                if (hudi.shunzi[s][m] > 1) beikou++;
            }
        }
        if (beikou == 2)        return [{ name: '二盃口', fanshu: 3 }];
        return [];
    }

一盃口とほぼ同じだが「盃口数」が2の場合は二盃口

清一色

    function qingyise() {
        for (var s of ['m','p','s']) {
            var yise = new RegExp('^[z' + s + '].*$');
            if (mianzi.filter(function(m){return m.match(yise)}).length
                        == mianzi.length
                &&  hudi.n_zipai == 0)
                    return [{ name: '清一色', fanshu: (hudi.menqian ? 6 : 5) }];
        }
        return [];
    }

混一色とほぼ同じだが字牌面子がない場合は清一色

後は役満。一気にいきます。

国士無双

    function guoshiwushuang() {
        if (mianzi.length != 13)    return [];
        if (hudi.danqi)     return [{ name: '国士無双十三面', fanshu: '**' }];
        else                return [{ name: '国士無双', fanshu: '*' }];
    }

面子数が13の場合は国士無双。さらに単騎待ちの場合は国士無双十三面。国士無双十三面はダブル役満にした。

四暗刻

    function sianke() {
        if (hudi.n_ankezi != 4)     return [];
        if (hudi.danqi)     return [{ name: '四暗刻単騎', fanshu: '**' }];
        else                return [{ name: '四暗刻', fanshu: '*' }];
    }

刻子が4つの場合は四暗刻。さらに単騎待ちの場合は四暗刻単騎。四暗刻単騎はダブル役満

大三元

    function dasanyuan() {
        if (hudi.kezi.z[5] + hudi.kezi.z[6] + hudi.kezi.z[7] == 3) {
            var bao_mianzi = mianzi.filter(function(m){
                        return m.match(/^z([567])\1\1(?:[\-\+\=]|\1)(?!\!)/)});
            var baojia = (bao_mianzi[2] && bao_mianzi[2].match(/[\-\+\=]/));
            return [{ name: '大三元', fanshu: '*', baojia: baojia && baojia[0] }];
        }
        return [];
    }

三元牌刻子が3つの場合大三元なのだが、大三元にはパオがあるのでパオに該当するかもチェックしている。暗槓を含む三元牌副露が3つある場合、その3つ目を鳴かせたものをbaojia:に設定する。mianzi

 [ 'm11', 'p789=!', 'z777=', 'z5555', 'z666+6']

の場合、baojia:'+' になる。

四喜和

    function sixihu() {
        var kezi = hudi.kezi;
        if (kezi.z[1] + kezi.z[2] + kezi.z[3] + kezi.z[4] == 4) {
            var bao_mianzi = mianzi.filter(function(m){
                        return m.match(/^z([1234])\1\1(?:[\-\+\=]|\1)(?!\!)/)});
            var baojia = (bao_mianzi[3] && bao_mianzi[3].match(/[\-\+\=]/));
            return [{ name: '大四喜', fanshu: '**', baojia: baojia && baojia[0] }];
        }
        if (kezi.z[1] + kezi.z[2] + kezi.z[3] + kezi.z[4] == 3
            && mianzi[0].match(/^z[1234]/))
                            return [{ name: '小四喜', fanshu: '*' }];
        return [];
    }

風牌刻子が4つの場合は大四喜(ダブル役満)。風牌刻子が3つで雀頭風牌の場合は小四喜大四喜もパオ対象の役なので、大三元同様にチェックしている。

字一色

    function ziyise() {
        if (hudi.n_zipai == mianzi.length)
                            return [{ name: '字一色', fanshu: '*' }];
        return [];
    }

全面子が字牌面子の場合は字一色字一色七対子形の場合があるので注意。*1

緑一色

    function lvyise() {
        if (mianzi.filter(function(m){return m.match(/^[mp]/)}).length > 0)
                                            return [];
        if (mianzi.filter(function(m){return m.match(/^z[^6]/)}).length > 0)
                                            return [];
        if (mianzi.filter(function(m){return m.match(/^s.*[1579]/)}).length > 0)
                                            return [];
        return [{ name: '緑一色', fanshu: '*' }];
    }

緑一色も一色手なので混一色のようにパターンマッチを使う。(1)萬子、筒子が含まれていれば緑一色ではない、(2)發以外の字牌が含まれていれば緑一色ではない、(3)一五七九索が含まれていれば緑一色ではない、の3つのチェックをパスすれば緑一色。

清老頭

    function qinglaotou() {
        if (hudi.n_kezi == 4 && hudi.n_yaojiu == 5 && hudi.n_zipai == 0)
                            return [{ name: '清老頭', fanshu: '*' }];
        return [];
    }

刻子が4つあり、幺九牌を含む面子が5つあり、字牌面子がない場合、清老頭刻子4つの条件を忘れると純全帯幺九を清老頭に誤判定するので注意。清老頭には七対子形はないのでそれに対する考慮は不要。

四槓子

    function sigangzi() {
        if (hudi.n_gangzi == 4)
                            return [{ name: '四槓子', fanshu: '*' }];
        return [];
    }

槓子が4つの場合は四槓子四槓子にパオは適用しないこととした。

九蓮宝燈

    function jiulianbaodeng() {
        if (mianzi.length != 1)             return [];
        if (mianzi[0].match(/^[mps]1112345678999/))
                            return [{ name: '純正九蓮宝燈', fanshu: '**' }];
        else                return [{ name: '九蓮宝燈', fanshu: '*' }];
    }

面子が1つの場合は九蓮宝燈和了牌以外が 1112345678999 の場合は純正九蓮宝燈のダブル役満

次回は最終回。和了点計算です。

*1:これを大七星のダブル役満にしようかと思ったが、一般的ではないのでやめた