1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C++初心者が待ち牌検出してみた

Posted at

#はじめに
C++を使って麻雀の待ち牌検出できないかなーと思って、Qiitaをざーっと見てみました。しかし、アルゴリズムだけの記述やpythonで作っている方々はいるのですがC++でコードを載せている方がいなかったので投稿してみようかなと思いました。
タイトルにもある通りC++初心者なのでプログラムは__for文,if文,配列のみ__で構成されています(350行くらいになりましたが笑)
Qiita初投稿なので見にくい部分があるかもしれませんが最後まで見ていただけると嬉しいです。

#待ち牌検出の考え方
待ち牌検出のアルゴリズムを考えていくわけですが

1.牌をツモってくる
2.雀頭を削除する
3.順子、刻子を削除する
4.全部消せたら待ち牌とする

という流れにしました。
3に関しては以下の性質を利用して面子を探します

例1)
tehai[m],tehai[m + 1],tehai[m + 2],tehai[m + 3]が
0枚、1枚、1枚以上、1枚以上
の場合は
tehai[m + 1],tehai[m + 2],tehai[m + 3]が順子として使われる

例2)
tehai[m],tehai[m + 1],tehai[m + 2],tehai[m + 3]が
0枚、2枚、2枚以上、2枚以上
の場合は
tehai[m + 1],tehai[m + 2],tehai[m + 3]が2組の順子として使われる(七対子は考えない)

例3)
tehai[m],tehai[m + 1]が
0枚、3枚以上
の場合は
tehai[m + 1]が刻子として使われる

例1~3を用いて面子を探し引いていきます

実際のプログラムではもう少し複雑なことをしているので1つ1つ解説していきたいと思います

#プログラム解説(関数、配列作成)
ここでは関数や配列を作成していきます

待ち牌検出.cpp
#include <iostream>
//コピー用関数
void copy(int a[], int b[]) {
	for (int i = 0; i < 38; i++) {
		a[i] = b[i];
	}
}
//待ち表示用関数
void show(int i) {
	if (i > 0 && i < 10) {
		std::cout << i << "m" << " ";
	}
	else if (i > 10 && i < 20) {
		std::cout << i - 10 << "s" << " ";
	}
	else if (i > 20 && i < 30) {
		std::cout << i - 20 << "p" << " ";
	}
	else if (i == 31) {
		std::cout << "東" << " ";
	}
	else if (i == 32) {
		std::cout << "南" << " ";
	}
	else if (i == 33) {
		std::cout << "西" << " ";
	}
	else if (i == 34) {
		std::cout << "北" << " ";
	}
	else if (i == 35) {
		std::cout << "白" << " ";
	}
	else if (i == 36) {
		std::cout << "發" << " ";
	}
	else if (i == 37) {
		std::cout << "中" << " ";
	}
}

//本文
int main(void) {
	int tehai0[38];
	int tehai[38];
	int machi[38];
	int tsumo[38];
	int fifth[38];
	int count = 0;
	int mentsu = 0;

	//初期化
	for (int i = 0; i < 38; i++) {
		tehai0[i] = 0;
		machi[i] = 0;
		tehai[i] = 0;
		fifth[i] = 0;
		tsumo[i] = 0;
	}

関数copy():tehai0[38]をtehai[38]にコピーするときなどに使います
関数show():待ち牌を表示するときに使います
tehai0[38]:ここに手牌を入力します(人力で)
tehai[38]:雀頭や面子削除したり、いじるのに使います
machi[38]:最終的な待ち牌を全部ここに入力します
fifth[38]:5枚目の牌は待ちにできないので、除外するために使います
tsumo[38]:手牌の状態から1枚ツモってきた状態をここに保存します
count:完成した面子の数を数えます
mentsu:手牌の枚数から何個面子ができるか数えます
例)手牌枚数13 → 面子数4
  手牌枚数10 → 面子数3
  手牌枚数7  → 面子数2
  手牌枚数4  → 面子数1
  手牌枚数1  → 面子数0

#プログラム解説(手牌入力~手牌表示)
先ほどの続きになります
ここでは手牌入力(ちょっと面倒)と手牌表示
とちょっとした処理をします

待ち牌検出.cpp
//牌入力
	/*使わない
	tehai0[0] = 0;
	tehai0[10] = 0;
	tehai0[20] = 0;
	tehai0[30] = 0;
	*/
	//萬子
	tehai0[1] = 3;
	tehai0[2] = 0;
	tehai0[3] = 0;
	tehai0[4] = 0;
	tehai0[5] = 0;
	tehai0[6] = 0;
	tehai0[7] = 0;
	tehai0[8] = 0;
	tehai0[9] = 0;
	//索子
	tehai0[11] = 0;
	tehai0[12] = 1;
	tehai0[13] = 1;
	tehai0[14] = 0;
	tehai0[15] = 0;
	tehai0[16] = 0;
	tehai0[17] = 0;
	tehai0[18] = 0;
	tehai0[19] = 2;
	//筒子
	tehai0[21] = 0;
	tehai0[22] = 0;
	tehai0[23] = 0;
	tehai0[24] = 0;
	tehai0[25] = 0;
	tehai0[26] = 1;
	tehai0[27] = 1;
	tehai0[28] = 1;
	tehai0[29] = 0;
	//字牌
	tehai0[31] = 0;//東
	tehai0[32] = 0;//南
	tehai0[33] = 3;//西
	tehai0[34] = 0;//北
	tehai0[35] = 0;//白
	tehai0[36] = 0;//發
	tehai0[37] = 0;//中

	//各種類枚数カウント(手牌表示で使う)
	int nm = 0,ns = 0,np = 0;
	for (int i = 1; i < 10; i++) {
		if (tehai0[i] > 0) {
			nm = nm + tehai0[i];
		}
	}
	for (int i = 10; i < 20; i++) {
		if (tehai0[i] > 0) {
			ns = ns + tehai0[i];
		}
	}
	for (int i = 20; i < 30; i++) {
		if (tehai0[i] > 0) {
			np = np + tehai0[i];
		}
	}

	//手牌表示
	std::cout << "手牌" << std::endl;
	for (int i = 1; i < 38; i++) {
		if (i == 10 && nm > 0) {
			std::cout << "m";
		}
		else if (i == 20 && ns > 0) {
			std::cout << "s";
		}
		else if (i == 30 && np > 0) {
			std::cout << "p";
		}
		if (tehai0[i] > 0) {
			for (int j = 0; j < tehai0[i]; j++) {
				if (i > 0 && i < 10) {
					std::cout << i;
				}
				else if (i > 10 && i < 20) {
					std::cout << i - 10;
				}
				else if (i > 20 && i < 30) {
					std::cout << i - 20;
				}
				else if (i == 31) {
					std::cout << "東";
				}
				else if (i == 32) {
					std::cout << "南";
				}
				else if (i == 33) {
					std::cout << "西";
				}
				else if (i == 34) {
					std::cout << "北";
				}
				else if (i == 35) {
					std::cout << "白";
				}
				else if (i == 36) {
					std::cout << "發";
				}
				else if (i == 37) {
					std::cout << "中";
				}
			}
		}
	}
	std::cout << std::endl;

	//コピー(待ち牌検索で使う)
	copy(tehai, tehai0);

	//5枚目エラー処理
	for (int i = 0; i < 38; i++) {
		if (tehai[i] > 4) {
			std::cout << "5枚目" << std::endl;
			return 0;
		}
	}

前半部分はただの手牌入力です。
何を何枚持っているのかを入力します。

nm,ns,npを使って手牌にある萬子、索子、筒子の数を数えています
これは後で手牌表示の際に使うのですが、見栄えを良くしているだけです。自己満ですね。

さっき入力をした手牌情報を表示します
コードの例では以下のように表示されます

手牌
111m2399s678p西西西

つまり萬子の111、索子の2399、筒子の678、字牌の西西西を持っていると分かります。

あとの処理はtehai0をtehaiに移したり、手牌に5を入力していたら誤入力なので終了します。もう一度言います。5入力は誤入力。・・・
(´・ω・`)

#プログラム解説(待ち牌検出)
さあ気を取り直して待ち牌検出についてです
ちょっと{}の量がえげつないのでご了承ください

待ち牌検出.cpp
//多牌・少牌確認
	int sum = 0;
	for (int i = 0; i < 38; i++) {
		sum = sum + tehai[i];
	}

	if (sum % 3 == 1 && sum <= 13) {

		//面子数入力
		mentsu = (sum - 1) / 3;

		for (int n = 1; n < 38; n++) {

			//10,20,30は処理スキップ
			if (n == 10 || n == 20 || n == 30) {
				continue;
			}
			//ツモる
			tehai[n]++;

			//5枚目の待ち除外

			if (tehai[n] == 5) {
				fifth[n] = 1;
			}

			//ツモ後の手牌保存
			copy(tsumo, tehai);

			//雀頭を探す
			for (int i = 1; i < 38; i++) {

				if (tehai[i] >= 2) {

					//雀頭消去
					tehai[i] = tehai[i] - 2;

					//mentsuの数だけ面子検出
					for (int j = 0; j < mentsu; j++) {

						//順子探索
						for (int m = 0; m < 27; m++) {
							if (tehai[m] == 0 && tehai[m + 1] == 1 && tehai[m + 2] >= 1 && tehai[m + 3] >= 1) {
								tehai[m + 1]--;
								tehai[m + 2]--;
								tehai[m + 3]--;
								count++;
							}
							if (tehai[m] == 0 && tehai[m + 1] == 2 && tehai[m + 2] >= 2 && tehai[m + 3] >= 2) {
								tehai[m + 1] -= 2;
								tehai[m + 2] -= 2;
								tehai[m + 3] -= 2;
								count += 2;
							}
						}

						//刻子探索
						for (int m = 0; m < 38; m++) {
							if (tehai[m] == 0 && tehai[m + 1] >= 3) {
								tehai[m + 1] -= 3;
								count++;
							}
						}
					}

					//待ち牌認定
					if (count == mentsu) {
						machi[n] = 1;
					}
					else {
						//ツモ後の手牌復元
						copy(tehai, tsumo);
					}
					//リセット
					count = 0;
				}
			}
			//元手牌の復元→違うツモの場合
			copy(tehai, tehai0);
		}
	}
	//多牌・少牌の場合
	else {
		std::cout << "牌の数が合っていません" << std::endl;
		return 0;
	}

まず手牌枚数を確認するためにsumに合計枚数を入れます
そのsumを3で割って余りが1でなければ多牌か少牌をしていることになります
sumから面子数を割り出してmentsuに入れます

ここからアルゴリズムの解説です
① tehai[10],[20],[30]には何も情報がないのでスキップします
② まず1つnをツモります(34種類について繰り返す)
③ この時点で5枚目の牌が出てきたらfifth[n]に1を入れます
④ ツモ後の今の状態をtsumoに保存します
⑤ ツモ後の状態から2枚以上ある牌を探して2引きます(34種類について繰り返す)
⑥ 雀頭を抜いたら面子を探します
⑦ この時に引いた面子の分だけcountに1足していきます
⑧ 面子が無くなりcountがmentsuと等しければ待ち牌としてカウントします
⑨ 等しくなければツモ後の手牌を復元し、countをリセットして⑤に戻って別の雀頭を探します
⑩ 全ての雀頭について調べたら入力時の手牌を復元し
⑪ ②に戻って次のツモについて調べます
⑫ 34種類のツモについて調べたら終了

#プログラム解説(待ち牌表示)
長くなりましたがラストです!

待ち牌検出.cpp
//待ち表示
	std::cout << "待ち" << std::endl;
	for (int i = 1; i < 38; i++) {
		machi[i] = machi[i] - fifth[i];
		if (machi[i] == 1) {
			show(i);
		}
	}
	//七対子判定
	int chitoi = 0;
	for (int i = 0; i < 38; i++) {
		if (tehai[i] == 2) {
			chitoi++;
		}
	}
	if (chitoi == 6) {
		for (int i = 0; i < 38; i++) {
			if (tehai[i] == 1) {
				std::cout << "七対子" << std::endl;
				show(i);
				std::cout << std::endl;
			}
		}
	}
	//国士無双判定
	int kokushi[13][2];
	int musou = 0, musou2 = 0;
	kokushi[0][0] = tehai0[1];
	kokushi[1][0] = tehai0[9];
	kokushi[2][0] = tehai0[11];
	kokushi[3][0] = tehai0[19];
	kokushi[4][0] = tehai0[21];
	kokushi[5][0] = tehai0[29];
	kokushi[6][0] = tehai0[31];
	kokushi[7][0] = tehai0[32];
	kokushi[8][0] = tehai0[33];
	kokushi[9][0] = tehai0[34];
	kokushi[10][0] = tehai0[35];
	kokushi[11][0] = tehai0[36];
	kokushi[12][0] = tehai0[37];
	kokushi[0][1] = 1;
	kokushi[1][1] = 9;
	kokushi[2][1] = 11;
	kokushi[3][1] = 19;
	kokushi[4][1] = 21;
	kokushi[5][1] = 29;
	kokushi[6][1] = 31;
	kokushi[7][1] = 32;
	kokushi[8][1] = 33;
	kokushi[9][1] = 34;
	kokushi[10][1] = 35;
	kokushi[11][1] = 36;
	kokushi[12][1] = 37;

	for (int i = 0; i < 13; i++) {
		if (kokushi[i][0] == 1) {
			musou++;
		}
		if (kokushi[i][0] == 0) {
			musou2 = kokushi[i][1];
		}
	}

	if (musou == 11) {
		std::cout << "国士無双" << std::endl;
		show(musou2);
	}
	else if (musou == 13) {
		std::cout << "国士無双" << std::endl;
		std::cout << "1m 9m 1s 9s 1p 9p 東 南 西 北 白 發 中" << std::endl;
		return 0;
	}
	std::cout << std::endl;
}

あとはmachi[38]を出力するだけなのですが
5枚目の待ち情報がfifth[38]にあるので引く
machi[0]~[37]で数字が1だったら関数show()を用いて表示

これで出力は以下のようになります

待ち
1s 4s

七対子について

2枚ある牌を探しchitoiに1を足します
chitoiが6であれば浮いている1枚の牌を待ち牌として表示します

国士無双について

kokushi[13][2]を作り
19字牌の枚数をkokushi[n][0]に
対応するtehai0の番号をkokushi[n][1]に入れていきます

kokushi[0][0]~[12][0]について
1枚だったらmusouに1を足し
0枚だったらmusou2にkokushi[n][1]を入れます(musouが11のときにしか使わない)

musouが11の場合はmusou2を関数show()に入れて表示します
musouが13の場合は1m 9m 1s 9s 1p 9p 東 南 西 北 白 發 中と表示します

#最後に
色々と書きすぎて無駄に長くなってしまいましたね。
解説読むの面倒だなと思った方はプログラム部分だけコピペして使ってください笑
今回は待ち牌検出のみの機能でしたが
翻数、点数を出力する機能も付けていきたいと思います。
あとはOpenCVやWebカメラを用いて実際の麻雀牌を用いた待ち牌検出もやってみたので
そっちもいつか記事にしようと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?