ヤマカサのプログラミング勉強日記

プログラミングに関する日記とどうでもよい雑記からなるブログです。

麻雀の和了形について

麻雀の和了形のパターン

配牌のパターン数は前回の記事で簡単に求まることが分かりましたが,和了形のパターンを求めるには労力が要ります.今回は基本的に下記の記事のことをやります.

[麻雀]天和の確率計算 - Qiita

麻雀の数学

1色手のパターンの列挙

1色手のパターンを列挙することができれば,一般手のパターンの列挙を行うことができます.

 i 枚の数牌が面子分解可能であるときのパターン数を  S(i, 0) とします.面子分解可能とは  i \bmod 3 = 0 を満たすとき  i / 3 個の面子を構成できることとします.次に  i 枚の数牌が和了形であるときのパターン数を  S(i, 1) とします.和了形とは,  i \bmod 3 = 2 を満たすとき  \min(1, i) 個の雀頭 \max(0, (i-2)/3) 個の面子を構成できることとします.

ここで, S(0, 0) = 1 かつ  S(0, 1) = 1 であることに注意します.

どうように字牌について  Z(i, 0), Z(i, 1) を定義します.このとき, 0 \leq i \leq 14 を満たす  i について  S, Z再帰的に求めることが可能です.

例えば  S(14, 1)清一色和了形のパターン数を表します.

一般手のパターンの列挙

萬子,筒子,索子,字牌の枚数をそれぞれ  i, j, k, l とします.また

 i + j + k + l = 14

を満たします.和了形を満たす  i, j, k, l の条件は1種類のみ雀頭を持ち,その他は面子分解可能であるときです.例えば,萬子で雀頭ができているとき,

 S(i, 1)S(j, 0)S(k, 0)Z(l, 0)

が一般手の和了形のパターン数となります.したがって, i, j, k, l について全探索することで一般手の和了パターン数を数え上げます.

二盃口のパターン数

 i (i \bmod 2 = 0) 枚の牌が面子分解可能または和了形であるとき, i/2 個の対子を構成できるときのパターン数を  S(i, 2) とします.

和了形を満たす  i, j, k, lについて

 S(i, 2)S(j, 2)S(k, 2)Z(l, 2)

二盃口のパターン数となります.

七対子のパターン数

二盃口のパターンを含めて考えると,

 _{34}C_{7}

となります.

国士無双のパターン数

 13

麻雀の和了形のパターン数

一般手のパターン数 + 七対子のパターン数 - 二盃口のパターン + 国士無双

となります.

コード

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}');
}