はじめに
あがり判定は麻雀において最も基本的なプログラムです。打牌選択と絡める場合など速度が要求されことがあるので、少しでも高速なアルゴリズムが必要です。本記事では四面子一雀頭形のあがり判定アルゴリズムを順を追って説明していきたいと思います。本質的に他の方と同じアルゴリズムです。
一色のあがり判定
まずは簡単のため一色に限定して話を進めます。あがり判定の流れは次のようになります。
- 手牌から雀頭候補を見つけて抜き出す。
- 残りの牌が面子の組に分解できるか判定する。
- ステップ2で面子の組に分解できれば「あがり」として終了、そうでなければ雀頭候補を手牌に戻してステップ1へ戻る。
- 「あがりではない」として終了する。
前提
長さ9のint型配列handを用意する。hand[0]からhand[8]の各要素に一マンから九マンの枚数を格納する。例えば牌姿と配列は以下のように対応します。
- 11123456788999 ⇔ 311111123
- 11234777888999 ⇔ 211100333
面子の組へ分解できるか判定
先にこちらを説明します。配列handの要素を0番目から見ていって、3で割った余りの数だけ次の要素と次の次の要素から引くことを繰り返します。__この操作が最後までできるときに限り手牌は面子の組に分解できます。__この操作は左から刻子、順子の順で抜いていくことと同じ結果を与えます。以下のコードは手牌の面子分解が可能かを判定する関数で、分解可能の場合はtrueを、分解不可能の場合はfalseを返します。
bool iswh0(const int* hand)
{
int r, a = hand[0], b = hand[1];
for(int i=0; i<7; i++){
if(r=a%3, b>=r && hand[i+2]>=r){
a=b-r; b=hand[i+2]-r;
}
else return false;
}
if(a%3==0 && b%3==0) return true;
else return false;
}
雀頭候補の探索
2枚以上ある牌を全て雀頭候補とみなしてもいいですが、より効率的な方法を紹介します。
まずは情報を整理します。
- 順子は連続した3つの牌から成る
- 刻子は同じ3つの牌から成る
- 雀頭は同じ2つの牌から成る
面子を構成する牌の和は3で割り切ることができるので、牌の和を3で割った余りに注目すれば雀頭候補を最大で3通りに絞り込めます。上の例では、
- 11123456788999 ⇒ 和は73, 3で割った余りは1 ⇒ 1を2で割って2 ⇒ 雀頭は258のどれか
- 11234777888999 ⇒ 和は83, 3で割った余りは2 ⇒ 2を2で割って1 ⇒ 雀頭は147のどれか
のようになります。1を2で割るときには3を足してから割ることに注意します。以下のコードは雀頭候補を絞りこみ面子分解判定を行う関数です。
bool iswh2(int* hand)
{
int p = 0;
for(int i=0; i<9; i++){
p += i*hand[i];
}
for(int i=p*2%3; i<9; i+=3){//ここでiを、牌の和を3で割った余りを2で割った数に初期化
hand[i] -= 2;
if(hand[i]>=0){
if(iswh0(hand)){
hand[i] += 2;
return true;
}
}
hand[i] += 2;
}
return false;
}
四色の場合へ拡張
四色の場合への拡張は先ほど定義した関数を使うことでできます。以下の関数islhでは長さ34の配列(0-8:マンズ1-9, 9-17:ピンズ1-9, 18-27:ソーズ1-9, 28-33:東南西北白発中)を受け取り、あがりのときにtrueを返します。ヘッダ__numeric
__のインクルードが必要です。accumulate
は配列の和を計算する関数です。
#include <numeric>
bool islh(int* hand)
{
int head = -1;
for(int i=0; i<3; i++){
switch(std::accumulate(hand+9*i,hand+9*i+9,0)%3){
case 1: return false;
case 2: if(head==-1) head = i; else return false;}
}
for(int i=27; i<34; i++){
switch(hand[i]%3){
case 1: return false;
case 2: if(head==-1) head = i; else return false;}
}
for(int i=0; i<3; i++){
if(i==head){if(!iswh2(hand+9*i)) return false;}
else{if(!iswh0(hand+9*i)) return false;}
}
return true;
}
メリットとデメリット
以上のアルゴリズムのメリットとデメリットをまとめます。
-
メリット:高速、枚数に依存しない
-
面子分解用に一時配列の確保を行わない
-
最小限の雀頭候補に対してしか面子分解の可能性判定を行わない
-
3N+2を満たす枚数であれば適用可能(鳴いて枚数が14枚以下でも対応できる、あるいは14枚以上でも対応できる)
-
デメリット:面子の分解は部分的にしか行わない
-
順子は取り出しているが刻子は取り出していない
-
雀頭の位置は常に決まるとは限らない
-
そのため得点計算に応用できない
デメリットについて補足します。例えば111222333m東東東南南南西西という手牌を考えます。この場合、マンズの部分は順子が3つ、または刻子が3つのどちらにとることもできますが、このアルゴリズムはその判断を行いません。麻雀のルール上、このような場合は得点が高い方の面子の分解パターンを採用します。そのため、この手牌では三暗刻が付くことを考慮して刻子が3つのパターンを採用します。
別の例も取り上げます。11223344m123p123sという手牌を考えます。この手牌では雀頭がどこかということが問題になります。この手牌に上述のアルゴリズムを適用した場合、一マンを雀頭とみなしてしまい、三色同順が見過ごされてしまいます。
つまり、和了判定でだけでなく得点計算を行うには、ある雀頭と面子の分解パターンを見つけて終了(このアルゴリズム)というのではなく、全ての雀頭と面子の分解パターンを求めて、それらに対して得点計算を行う必要があります。