和了形を求めるプログラム(特殊形) - koba::blog の続き。
一般形(4面子1雀頭)の和了形を求めるメイン関数は hule_mianzi_yiban()
。処理の流れは以下の通り。
- 2枚以上ある牌を雀頭候補として抜き取る
- 残りの牌で4面子構成できるか調べるために
mianzi_all()
を呼出す - 得られた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。
- 搭子を残す必要がない(搭子が残った場合は和了形ではない)
- 面子の数ではなく、とりうる面子の組合わせすべてを返す必要がある
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:関数名は同じだがスコープが違うので別物