麻雀の得点計算をするプログラムを書こうと思っているのだが、そのためには先に和了形を求めるプログラムを書く必要がある。というのも和了形は複数に解釈できる場合があり、その場合一番得点が高くなるパターンを選択する必要があるからだ。
和了形が複数に解釈できるパターンは以下の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の処理)。
これで準備はできたので、次は役判定プログラムを書く。