麻雀の和了形について
麻雀の和了形のパターン
配牌のパターン数は前回の記事で簡単に求まることが分かりましたが,和了形のパターンを求めるには労力が要ります.今回は基本的に下記の記事のことをやります.
1色手のパターンの列挙
1色手のパターンを列挙することができれば,一般手のパターンの列挙を行うことができます.
枚の数牌が面子分解可能であるときのパターン数を とします.面子分解可能とは を満たすとき 個の面子を構成できることとします.次に 枚の数牌が和了形であるときのパターン数を とします.和了形とは, を満たすとき 個の雀頭と 個の面子を構成できることとします.
ここで, かつ であることに注意します.
どうように字牌について を定義します.このとき, を満たす について を再帰的に求めることが可能です.
一般手のパターンの列挙
萬子,筒子,索子,字牌の枚数をそれぞれ とします.また
を満たします.和了形を満たす の条件は1種類のみ雀頭を持ち,その他は面子分解可能であるときです.例えば,萬子で雀頭ができているとき,
が一般手の和了形のパターン数となります.したがって, について全探索することで一般手の和了パターン数を数え上げます.
二盃口のパターン数
枚の牌が面子分解可能または和了形であるとき, 個の対子を構成できるときのパターン数を とします.
和了形を満たす について
が二盃口のパターン数となります.
七対子のパターン数
二盃口のパターンを含めて考えると,
となります.
国士無双のパターン数
麻雀の和了形のパターン数
一般手のパターン数 + 七対子のパターン数 - 二盃口のパターン + 国士無双
となります.
コード
import 'dart:math'; List<int> h = List.generate(9, (index) => 0); // 手牌 List<List<int>> sNum = List.generate(15, (index) => [0, 0, 0, 0]); List<List<int>> zNum = List.generate(15, (index) => [0, 0, 0, 0]); List<List<int>> sDen = List.generate(15, (index) => [0, 0, 0, 0]); List<List<int>> zDen = List.generate(15, (index) => [0, 0, 0, 0]); List<List<List<int>>> sHand = List.generate(15, (index) => []); List<List<List<int>>> zHand = List.generate(15, (index) => []); const int S = 9; // 数牌の数 const int K = 3; // 数牌の種類 const int Z = 7; // 字牌の種類 const int T = S * K + Z; // 牌の種類 34 const List<int> C = [1, 4, 6, 4, 1]; // 同種類の牌からi枚選ぶ時の場合の数 int comb(int n, int r) { if (r < 0 || n < r) return 0; r = min(r, n - r); if (r == 0) return 1; return comb(n - 1, r - 1) * n ~/ r; } int pow(int x, int n) { if (n == 0) return 1; if (n % 2 == 0) return pow(x * x, n ~/ 2); return x * pow(x, n - 1); } // 左から刻子,順子の順番で面子分解可能かどうか bool canDecompose(List<int> h) { int k1 = 0; int k2 = 0; for (int i = 0; i < 7; i++) { if (h[i] - k1 - k2 < 0) return false; int k0 = (h[i] - k1 - k2) % 3; if (h[i + 1] - k0 - k1 < 0 || h[i + 2] - k0 < 0) return false; k2 = k1; k1 = k0; } if ((h[7] - k1 - k2) % 3 != 0 || (h[8] - k1) % 3 != 0) return false; return true; } // 字牌 bool canDecomposeZ(List<int> h) { for (int i = 0; i < 7; i++) { if (h[i] % 3 != 0) return false; } return true; } // 和了かどうか bool isAgari(List<int> h) { int tot = 0; for (int i = 0; i < 9; i++) { tot += i * h[i]; } for (int i = ((tot % 3) * 2) % 3; i < 9; i += 3) { if (h[i] >= 2) { h[i] -= 2; if (canDecompose(h)) { h[i] += 2; return true; } h[i] += 2; } } return false; } // 字牌 bool isAgariZ(List<int> h) { for (int i = 0; i < 7; i++) { if (h[i] == 1 || h[i] == 4) return false; if (h[i] >= 2) { h[i] -= 2; if (canDecomposeZ(h)) { h[i] += 2; return true; } h[i] += 2; } } return false; } // 34種類の牌に対して和了かどうか bool isAgariAll(List<int> h) { List<int> t = List.generate(4, (index) => 0); for (int i = 0; i < 3; i++) { for (int j = 9 * i; j < 9 * (i + 1); j++) { t[i] += h[j]; } } for (int i = 0; i < 7; i++) { t[3] += h[27 + i]; } int cnt = 0; for (int i = 0; i < 4; i++) { if (t[i] % 3 == 1) return false; if (t[i] % 3 == 2) cnt++; } if (cnt != 1) return false; if (t[3] % 3 == 2) { if (!isAgariZ(h.sublist(27))) return false; for (int i = 0; i < 3; i++) { if (!canDecompose(h.sublist(9 * i, 9 * (i + 1)))) return false; } } else { if (!canDecomposeZ(h.sublist(27))) return false; for (int i = 0; i < 3; i++) { if (t[i] % 3 == 0) { if (!canDecompose(h.sublist(9 * i, 9 * (i + 1)))) return false; } else { if (!isAgari(h.sublist(9 * i, 9 * (i + 1)))) return false; } } } return true; } bool sevenpairs(List<int> h) { int cnt = 0; for (int i = 0; i < 9; i++) { if (h[i] == 2) cnt++; else if (h[i] != 0) return false; } return cnt == 7; } bool isToitu(List<int> h) { for (var item in h) { if (item != 0 && item != 2) return false; } return true; } // N: 牌の種類の数 n: 深さ優先探索の深度 m: 使用する牌の数, a: 現在の牌の数, h: 手牌 d: 状態数 void generateHand(int N, int n, int m, int a, List<int> h, int d) { if (a == m) { if (N == 7) { zNum[m][0]++; zDen[m][0] += d; if (m % 3 == 2) { if (isAgariZ(h)) { zNum[m][1]++; zDen[m][1] += d; if (isToitu(h)) { zNum[m][3]++; zDen[m][3] += d; } } } else if (m % 3 == 0) { if (canDecomposeZ(h)) { zNum[m][2]++; zDen[m][2] += d; if (isToitu(h)) { zNum[m][3]++; zDen[m][3] += d; } } } //zHand[m].add([...h]); } else { sNum[m][0]++; sDen[m][0] += d; if (m % 3 == 2) { if (isAgari(h)) { sNum[m][1]++; sDen[m][1] += d; if (isToitu(h)) { sNum[m][3]++; sDen[m][3] += d; } } } else if (m % 3 == 0) { if (canDecompose(h)) { sNum[m][2]++; sDen[m][2] += d; if (isToitu(h)) { sNum[m][3]++; sDen[m][3] += d; } } } //sHand[m].add([...h]); } return; } if (n == N) return; for (int i = 4; i >= 0; i--) { if (a + i <= m) { h[n] = i; generateHand(N, n + 1, m, a + i, h, d * C[i]); h[n] = 0; } } } bool isAgariNum(int i, int j, int k, int l) { if (i % 3 == 1 || j % 3 == 1 || k % 3 == 1 || l % 3 == 1) return false; if (i % 3 == 2 && j % 3 == 0 && k % 3 == 0 && l % 3 == 0) return true; if (i % 3 == 0 && j % 3 == 2 && k % 3 == 0 && l % 3 == 0) return true; if (i % 3 == 0 && j % 3 == 0 && k % 3 == 2 && l % 3 == 0) return true; if (i % 3 == 0 && j % 3 == 0 && k % 3 == 0 && l % 3 == 2) return true; return false; } void main(List<String> args) { List<int> h = List.generate(9, (index) => 0); for (int i = 0; i < 15; i++) { generateHand(9, 0, i, 0, h, 1); generateHand(7, 0, i, 0, h, 1); } int cTot = 0, pTot = 0, aPTot = 0, aCTot = 0, rPTot = 0, rCTot = 0; for (int i = 14; i >= 0; i--) { for (int j = 14 - i; j >= 0; j--) { for (int k = 14 - i - j; k >= 0; k--) { int l = 14 - i - j - k; pTot += sNum[i][0] * sNum[j][0] * sNum[k][0] * zNum[l][0]; cTot += sDen[i][0] * sDen[j][0] * sDen[k][0] * zDen[l][0]; if (isAgariNum(i, j, k, l)) { aPTot += sNum[i][(i % 3 == 2 ? 1 : 2)] * sNum[j][(j % 3 == 2 ? 1 : 2)] * sNum[k][(k % 3 == 2 ? 1 : 2)] * zNum[l][(l % 3 == 2 ? 1 : 2)]; aCTot += sDen[i][(i % 3 == 2 ? 1 : 2)] * sDen[j][(j % 3 == 2 ? 1 : 2)] * sDen[k][(k % 3 == 2 ? 1 : 2)] * zDen[l][(l % 3 == 2 ? 1 : 2)]; rPTot += sNum[i][3] * sNum[j][3] * sNum[k][3] * zNum[l][3]; rCTot += sDen[i][3] * sDen[j][3] * sDen[k][3] * zDen[l][3]; } } } } int sPTot = comb(34, 7); int sCTot = sPTot * pow(6, 7); int kPTot = 13; int kCTot = kPTot * pow(4, 12) * 6; print('合計 ${pTot} ${cTot}'); print('一般形の和了形 ${aPTot} ${aCTot}'); print('二盃口 ${rPTot} ${rCTot}'); print('七対子 ${sPTot} ${sCTot}'); print('国士無双 ${kPTot} ${kCTot}'); print( '全ての和了形 ${aPTot + sPTot - rPTot + kPTot} ${aCTot + sCTot - rCTot + kCTot}'); }