以前 麻雀の役を判定するプログラム - koba::blog でも一度扱っているが、そのときは正規表現で役を判定していた。今回は 麻雀の符を求めるプログラム - koba::blog で面子構成を求めているのでそれを使って役を判定していく。
電脳麻将 の和了役判定関数は get_hupai(mianzi, hudi, pre_hupai)
。
入力は以下の通り。
mianzi
- 和了形を求めるプログラム(特殊形) - koba::blog、和了形を求めるプログラム(一般形) - koba::blog で求めた和了形の面子。
hudi
- 麻雀の符を求めるプログラム - koba::blog で求めた面子構成。
pre_hupai
- 麻雀の和了点の計算 〜 状況役と懸賞役の一覧を作る - koba::blog で求めた状況役一覧。
和了役判定関数をのぞいた全体の構造は以下の通り。
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 []; }
三暗刻
function sananke() { if (hudi.n_ankezi == 3) return [{ name: '三暗刻', fanshu: 2 }]; return []; }
三槓子
function sangangzi() { if (hudi.n_gangzi == 3) return [{ name: '三槓子', fanshu: 2 }]; return []; }
三色同刻
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 []; }
混老頭
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 []; }
混一色
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 []; }
二盃口
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 []; }
清一色
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: '*' }]; }
四暗刻
function sianke() { if (hudi.n_ankezi != 4) return []; if (hudi.danqi) return [{ name: '四暗刻単騎', fanshu: '**' }]; else return [{ name: '四暗刻', fanshu: '*' }]; }
大三元
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 []; }
緑一色
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 []; }