麻雀C言語プログラム集



MENU


MJAIクライアント

MJAIクライアントとはmjai上で動くAIです。参考


「mjai-occam0.524」(天鳳R1800 特上条件到達)
  ↑のソースコード
・XCode8.2で開発しています
・実行ファイルの下に opponenthoraestimator/exeと同じ階層に置くファイル 以下のファイルを配置する必要があります
・mjaigame/MJAIGame.xcodeprojがメインのプロジェクトファイルです
・個人制作で特に配布を意識して作られたものではないのでソースとかは汚いし説明も少ないです(とりあえずバックアップ目的で公開したものです)。
・もし使いたかったらソースとか読み解いてご自由に。
        
「mjai-occam0.51」(天鳳R1700)


製作物

麻雀得点計算C++ソース「MJScore.h」
MJexeIO.DLL読込関数
モンテカルロ法で相手の放銃率と放銃点を求めるC++クラス「MonteCarloDangerEstimator.h」(質は悪い)
【iOS】麻雀得点計算アプリのソースコード


実験とか

棒聴即リー全ツッパプログラムのレートはいくつか?
【レポート】自作麻雀AI「Occam」
機械学習的な手法による麻雀AI開発の試行
モンテカルロ法による相手の得点予想
劣化爆打は作れるだろうか?〜テンパイ率推定編〜
麻雀AI用CHITEST(カイ二乗検定)

 


雑記

個人的麻雀プログラムメモ
麻雀プログラム思った事


リンク

掲示板(バグ報告等ご自由に)
麻雀用語一覧(Wikipedia)
リンク集


前提

前提


基本関数

シャンテン数(あがり判定を含む)
有効牌
牌山
リーチ後の暗カン禁止


得点計算

方針
手配のメンツ分解
符の計算


ダブル役満

国士無双13面待
九蓮宝燈9面待
大四喜和
四暗刻単騎


役満

字一色
四暗刻
緑一色
小四喜和
大三元
九蓮宝燈
清老頭


一飜役

平和
断ヤオ
一盃口
飜牌(白、発、中)


二飜役(鳴いて一飜役)

全帯
三色同順
一気通貫


二飜役

対々和
三色同刻
混老頭
三暗刻
小三元


三飜役(面前役)

二盃口


三飜役(鳴いて二飜役)

混一色
純全帯


六飜役(鳴いて伍飜役)

清一色


※以下の役は手牌からだけでは判断できないので掲載していません

ドラ リーチ 一発 門前清模和 槍槓 海底撈月 河底撈魚 嶺上開花
ダブルリーチ 流し満貫 天和 地和 飜牌(自風、場風) 三槓子 四槓子


リンク集 

現代麻雀技術論様作成 麻雀サイトの紹介
mjscore.txtの書式について
麻雀の数学
雀賢荘【データ室】強者は何が違うのか?
じどうでまーじゃん
Mjai 麻雀AI対戦サーバ
統計による麻雀危険牌分析
gimite/mjai-manue
「まうじゃん」対戦相手プラグイン インターフェース仕様
【科学する麻雀】副露別テンパイ率
一人麻雀シミュレーションデータの概要
一人麻雀不聴時の期待値計算法
DLLの環境設定
GPW論文
ニコニコ [麻雀]コンピュータで鳳凰卓を目指すpart1[天鳳]
ニコニコ 【麻雀】評価関数で天鳳自動打ちpart1【天鳳】
麻雀プログラムに関するリンク集
floodgate for mahjong
MathJax
mjaiのwikiがスパムで消えてたのでミラー


↓↓↓↓↓↓↓ここから本文↓↓↓↓↓↓↓


前提 

参考:とつげき東北氏作MJexeIO.DLL

牌の表現方法

牌番号 牌種
1・・・・・・ 1マン
2・・・・・・ 2マン

9・・・・・・ 9マン
10・・・・・・未定義
11・・・・・・1ピン
12・・・・・・2ピン

19・・・・・・9ピン
20・・・・・・未定義
21・・・・・・1ソウ
22・・・・・・2ソウ

29・・・・・・9ソウ
30・・・・・・未定義
31・・・・・・東
32・・・・・・南
33・・・・・・西
34・・・・・・北
35・・・・・・白
36・・・・・・発
37・・・・・・中

フーロ牌の表現方法

フーロ牌はint型の配列fuuro[20]に、n+1番目のフーロ牌について

と定義しています。
当HPの全ての関数でint fuuro[20]がグローバル変数として定義してあるものとしています。
他、基本的に第一東風荘のルールに準拠しています。


シャンテン数 

国士無双、チートイツ、通常手の3パターンにわけて計算します。

・国士無双
「13−ヤオチュウ牌の種類」で求めた数からいずれか1牌が2枚以上であった場合、1引きます。

・チートイツ
「6−トイツの数」で求めた数から4枚持ちを考慮して「7−牌の種類」を足します。

・通常手
バックトラック法を使います(参考(Wikipedia))。

頭を抜く

メンツを抜けるだけ抜く

ターツを抜けるだけ抜く

シャンテン数を計算する。他の組み合わせ以下だった場合、値を保持する。フーロがある場合はフーロ数×2を引く

これを全組み合わせ試す

という方針ですが、メンツ+ターツの数は4以下でなければいけません。よってシャンテン数を求める式は
「8−メンツ数×2−ターツ数―頭(1固定)」
となります。またシャンテン数が0の場合テンパイ、−1の場合和了ですのでテンパイチェック、和了チェックとしても使えます。

このアルゴリズムであらの(一人)麻雀研究所にある向聴数を求める練習問題を使って動作を確認しました→ソースダウンロード(VC++2008で確認)
全問正解していますが速度的にどうか疑問が残ります。しかし枚数を気にせず使用できるというメリットもあります。


----------------------------------------------
【追記】
下記プログラムを再利用しやすいようにクラス化(C++)してみた。
DL→syanten.h(ヘッダ)
syanten.cpp(本体、使用するソースと共にビルド)

使い方
1.syanten.hをincludeする
2.Syantenクラスのインスタンスを作る(Syanten instなど)
3.set_tehai(int[])メソッドで非フーロ牌をセット(サイズ38以上int配列ポインタ渡し、赤は統合済みである事)
4.set_fuurosuu(int)メソッドでフーロ数を入れる(初期値0)
5.NormalSyantenメソッドで通常手、KokusiSyanten()で国士、TiitoituSyanten()でチートイのシャンテン数が返る(全てint型)
6.Clear()メソッドで中身をクリアできる
----------------------------------------------


----------------------------------------------
【追記2】
こちらの方法で高速化したハッシュテーブル版

DL→syanten.h(ヘッダ)
syanten.cpp(本体、使用するソースと共にビルド)
syanten.dat(テーブルファイル(7.5MB)。exeと同階層にないと動かない。変更したい場合はsyanten.cppのコンストラクタ内でファイル指定できる。)

使い方は同上。

----------------------------------------------


----------------------------------------------
【ひとりごとメモ】
bitDP、メモ化再帰を使えばもっと高速化できそう。
要するにバックトラックで探索途中の状態をメモしておいて使い回せばいい。
1牌の状態(0~4枚)を3bit(000,001,010,011,100)で表せば
一種類につき3*9=27bitで表現できるからint型で表せる。
----------------------------------------------


------------------以下ソース------------------

/* * 国士無双のシャンテン数を返す */
int KokusiSyanten()
{
	int kokusi_toitu=0,syanten_kokusi=13,i;             
	//老頭牌
	for(i=1;i<30;i++){        
		if(i%10==1||i%10==9||i%20==1||i%20==9){	   
			if(tehai[i])
				syanten_kokusi--;
			if(tehai[i] >=2 && kokusi_toitu==0)
				kokusi_toitu=1;	
		}
	}             
	//字牌
	for(i=31;i<38;i++){      
		if(tehai[i]){	        
			syanten_kokusi--;
			if(tehai[i] >=2 && kokusi_toitu==0)
				kokusi_toitu=1;			            
		}
	}             
	//頭
	syanten_kokusi-= kokusi_toitu;             
	return syanten_kokusi;
}
/***************************************************************************************/
/* * チートイツのシャンテン数を返す */
int TiitoituSyanten()
{
	int i=1,toitu=0,syurui=0,syanten_tiitoi;
	//トイツ数を数える
	for(;i<=37;i++){ 
		for(;!tehai[i];i++);
		if(i>=38) continue;
		syurui++;
		if(tehai[i] >=2)
		toitu++;
	}
	syanten_tiitoi=6-toitu;
	//4枚持ちを考慮
	if(syurui<7)
		syanten_tiitoi+= 7-syurui;
	return syanten_tiitoi;
}
/***************************************************************************************/
/* * 通常手のシャンテン数を返す */
void mentu_cut(int);//メンツ抜き関数
void taatu_cut(int);//ターツ抜き関数
//作業用グローバル変数
int mentu;		//メンツ数
int toitu;		//トイツ数
int kouho;		//ターツ数
int fuurosuu;		//フーロ数
int temp;		//シャンテン数(計算用)
int syanten_normal;	//シャンテン数(結果用)
/***************************************************************************************/
//入口(この関数だけを呼びだす)
int NormalSyanten()
{
	//初期化
	mentu=0;
	toitu=0;
	kouho=0;
	temp=0;
	syanten_normal=8;
	for(int i=1;i<38;i++)
	{
		//頭抜き出し
		if(2 <= tehai[i])
		{            
			toitu++;
			tehai[i] -= 2;
			mentu_cut(1);
			tehai[i] += 2;
			toitu--;
		}
	}
	mentu_cut(1);   //頭無しと仮定して計算
	return syanten_normal;	//最終的な結果
}
/***************************************************************************************/
//メンツ抜き出し
void mentu_cut(int i)
{
	for(;!tehai[i];i++);
	if(i>=38){taatu_cut(1);return;}//メンツを抜き終わったのでターツ抜きへ
	//コーツ
	if(tehai[i]>=3)
	{
		mentu++;
		tehai[i]-=3;
		mentu_cut(i);
		tehai[i]+=3;
		mentu--;
	}
	//シュンツ
	if(tehai[i+1]&&tehai[i+2]&&i<30)
	{
		mentu++;
		tehai[i]--,tehai[i+1]--,tehai[i+2]--;
		mentu_cut(i);
		tehai[i]++,tehai[i+1]++,tehai[i+2]++;
		mentu--;
	}
	//メンツ無しと仮定
	mentu_cut(i+1);
}
/***************************************************************************************/
//ターツ抜き出し
void taatu_cut(int i)
{													   
	for(;!tehai[i];i++);
	if(i>=38) //抜き出し終了
	{
		temp=8-mentu*2-kouho-toitu;
		if(temp<syanten_normal) { syanten_normal=temp; }
		return;
	}
	if(mentu+kouho<4)
	{            
		//トイツ
		if(tehai[i]==2)
		{
			kouho++;
			tehai[i]-=2;
			taatu_cut(i);
			tehai[i]+=2;
			kouho--;
		}
	             
		//ペンチャンorリャンメン
		if(tehai[i+1]&&i<30)
		{
			kouho++;
			tehai[i]--,tehai[i+1]--;
			taatu_cut(i);
			tehai[i]++,tehai[i+1]++;
			kouho--;
		}
	             
		//カンチャン
		if(tehai[i+2]&&i<30&&i%10<=8)
		{
			kouho++;
			tehai[i]--,tehai[i+2]--;
			taatu_cut(i);
			tehai[i]++,tehai[i+2]++;
			kouho--;
		}
	}
	taatu_cut(i+1);
}
/* ***************************************************************************************  * 【問題点】  * 例えば「東東東東南南南南西西西北北北」のシャンテン数は1ですが  * このソースでは0(テンパイ)になってしまいます。 * これは4枚目の字牌はターツになる事ができない「死に孤立字牌」なのが理由です。これを回避するには  * 「どれか一つでも「4枚目の字牌」を持っていたらシャンテン数+1」とすれば良いかもしれませんが未検証です。  *************************************************************************************** */

有効牌 

有好牌をリストアップするプログラムです。
有効牌とは「シャンテン数を1さげる事ができる牌」の事です。つまりこれも国士無双、チートイツ、通常手の3パターンにわけて計算しなければいけません。
プログラムの方針としては単純に
・手配に孤立牌でない牌(国士は19字牌、チートイは同種牌)を加えシャンテン数を計算し、現在のシャンテン数より1下がればそれは有効牌である
というものを作れば良い事になります。
またシャンテン数が0=テンパイ時に有効牌と判定された牌はすなわち「待ち」になります。
以下のプログラムでは当HPにあるシャンテン数を計算する関数を使用しています。
※注意点
有効牌は通常、ツモ中でない時に計算しますのでこの関数をツモ中でない時に実行すると正しい結果が得られない場合があります。
ツモ中でない時は
・手配の枚数÷3の余り=1
という式を使えば判別できます。



----------------------------------------------
【追記】
下記プログラムを再利用しやすいようにクラス化(C++)してみた。
DL→yuukouhai.h(ヘッダ)
yuukouhai.cpp(本体、使用するソースと共にビルド)

使い方
1.yuukouhai.hをincludeする
2.Yuukouhaiクラスのインスタンスを作る(Yuukouhai instなど)
3.set_tehai(int[])メソッドで非フーロ牌をセット(サイズ38以上int配列ポインタ渡し、赤は統合済みである事)
4.結果用のC++STL vectorのvector型オブジェクトを用意する(vector resultなど)
5.以下メソッドにポインタ渡しで以下メソッドを呼び出すと結果が入る(NormalYuukou(&result)など)。メソッドの返り値(bool値)がtrueなら成功、falseならエラー

NormalYuukou(vector[])//通常手
KokusiYuukou(vector[])//国士
TiitoiYuukou(vector[])//チートイ

----------------------------------------------


------------------以下ソース------------------

int not_koritu[38];//孤立牌でない牌格納用グローバル変数
/* *int not_koritu[38]に「牌番号」を順に入れています */
void NotKoritu ()
{
	int work[38];//作業用
	int i=0,p=0;
	//配列初期化
	memset(work,0,sizeof(work));
	memset(not_koritu,0,sizeof(not_koritu));
	//計算
	for(;i<=37;i++){ 
		for(;!tehai[i];i++);if(i>=38) continue;
		//数牌
		if(i<30){
			if(i%10==1) work[i]=work[i+1]=work[i+2]=1;
			else if(i%10==2) work[i]=work[i+1]=work[i+2]=work[i-1]=1;
			else if(i%10>=3 &&i%10<=7) work[i]=work[i-1]=work[i+1]=work[i-2]=work[i+2] =1;
			else if(i%10==8) work[i]=work[i+1]=work[i-2]=work[i-1]=1;
			else if(i%10==9) work[i]=work[i-1]=work[i-2]=1;             
		}
		//字牌
		else if(i>30) work[i]=1;
	}
	//結果を格納
	for(i=0;i<=37;i++){ 
		for(;!work[i];i++);if(i>=38) continue;
				not_koritu[p++]=i;
	}
}
/***************************************************************************************/
/* * 通常手の有効牌 *int NormalYuukou [38]に「牌番号」を順に入れています */ 
void NormalYuukou (int NormalYuukou [])
{
	int p_koritu=0;		//配列の添え字ポインタ(孤立牌)
	int p_yuukou=0;		//配列の添え字ポインタ(有効牌)
	int syanten=NormalSyanten();   	//現在の牌のシャンテン数 
	memset(NormalYuukou,0,sizeof(NormalYuukou)*38);//初期化 
	while(not_koritu[p_koritu] != 0){
		//否孤立牌を追加
		tehai[not_koritu[p_koritu]]++;
		//現在値と比較
		if(NormalSyanten() <syanten)
			NormalYuukou [p_yuukou++]= not_koritu[p_koritu];
		//追加した否孤立牌をマイナス
		tehai[not_koritu[p_koritu]]--;
		p_koritu++;
	}
}
/***************************************************************************************/
/* * チートイツの有効牌 *int TiitoiYuukou [38]に「牌番号」を順に入れています */
void TiitoiYuukou (int TiitoiYuukou [])
{
	int i=0;//配列の添え字ポインタ(孤立牌)
	int p_yuukou=0;//配列の添え字ポインタ(有効牌)
	int syanten=TiitoituSyanten();  //現在の牌のシャンテン数 
	memset(TiitoiYuukou,0,sizeof(TiitoiYuukou)*38);//初期化 
	while(i<=37){
		//否孤立牌を追加
		for(;!tehai[i];i++);if(i>=38) continue;
		tehai[i]++;
		//現在値と比較
		if(TiitoituSyanten() <syanten)
		TiitoiYuukou [p_yuukou++]=i;
		//追加した否孤立牌をマイナス
		tehai[i]--;
		i++;
	}	
}
/***************************************************************************************/
/* * 国士無双の有効牌 *int KokusiYuukou [38]に「牌番号」を順に入れています */ 
void KokusiYuukou (int KokusiYuukou [])
{
	int p_yuukou=0;//配列の添え字ポインタ(有効牌)
	int i=0;
	int syanten=KokusiSyanten();//現在の牌のシャンテン数
	memset(KokusiYuukou,0,sizeof(KokusiYuukou)*38);//初期化
	int yaotyuu[13]={1,9,11,19,21,29,31,32,33,34,35,36,37};
	for(;i<13;i++){
		//否孤立牌を追加
		tehai[yaotyuu[i]]++;
		//現在値と比較
		if(KokusiSyanten() <syanten)	
			KokusiYuukou [p_yuukou++]= yaotyuu[i];
		//追加した否孤立牌をマイナス
		tehai[yaotyuu[i]]--;
	}
}

※応用

//フリテンチェック
//同巡フリテンは別に判定が必要です
//@return trueでフリテン
//@param1 tehai 非ツモ状態の手牌配列
//@param2 fuurosuu フーロ数
//@param3 vector 自分の河が入っているvector
bool isFuriten(int tehai[],int fuurosuu,vector kawa){
	Yuukouhai yuu;
	yuu.set_tehai(tehai);
	yuu.set_fuurosuu(fuurosuu);

	vector work;
	//通常手
	if (yuu.NormalSyanten()==0){
		yuu.NormalYuukou(&work);
	}
	//七対子
	else if (yuu.TiitoituSyanten()==0){
		yuu.TiitoiYuukou(&work);
	}
	//国士無双
	else if (yuu.KokusiSyanten()==0){
		yuu.KokusiYuukou(&work);
	}
	for(int i = 0;i < work.size();i++){
		for(int j = 0;j < kawa.size();j++){
			if(work[i] == kawa[j]){
				return true;
			}
		}
	}
	return false;
}


 

牌山 
先ず順に1マンから中まで4牌ずつ並べた136個の配列(yama[])を用意します。
次に[最後尾]と、[最後尾-1]からランダムに選んだ要素を交換します。
最後尾←最後尾-1とし、これが最後尾>0の間繰り返します。
疑似コードにすると下記のようになります。

var ← 配列の最後尾の添え字(135)
For var > 0の間
i← 0からvar-1までの中からランダムに1つ
yama[i]とyama[var]を交換
var ← var -1
Next

これで山ができたので王牌を最後尾から14牌とするなどの仕様を決める事ができます。
ちなみにリアル麻雀では王牌を決める時、積み込み防止のためサイコロを2回振るというルールがありますが、麻雀ゲームを作る際には演出として表示するだけで良いと思います。

・乱数について
C標準ライブラリのrand()は線形合同法という乱数アルゴリズムを使用していますが、これは質が良くありません。
そこでここでは良質&高速&フリーとして有名な乱数である「メルセンヌツイスター」を使用します。
仕組みを理解しようとすると筆者のような素人には手も足もでないので、以下単純な使い方だけ(0~134の乱数を生成する)説明します

1.メルセンヌツイスターのHPから“mt19937ar.c”が含まれているファイルをダウンロードします。
2.ビルドするプロジェクトなどに“mt19937ar.c”を含ませます。
3.最初に一度だけinit_genrand関数を呼び出します。標準Cで言うsrand()のようなもので、乱数の種が引数に必要です。一般的には現在の時間を入れます。
ex)init_genrand((unsigned)time(NULL)) //"time.h"をincludeすること

4.以下、おなじみのrand()関数と同様にgenrand_int32()を呼び出します。今回は0~134の乱数が欲しいので、135で割った余りを使用します
ex)var=genrand_init32()%135;


あとはメルセンヌ・ツイスタより周期が短いが早いXorShift(https://ja.wikipedia.org/wiki/Xorshift)とかあります
乱数参考(http://www001.upp.so-net.ne.jp/isaku/rand.html)


----------------------------------------------
【追記】
時間は常に変化しているので、種に時間を使うのは良いのですが、
乱数の種によく使われるtime関数は、秒単位の値を返します。
しかしコンピュータにとって1秒は”長い時間”です。処理によっては、早すぎて種を更新できない可能性があります。
というわけで種には、ミリ秒単位で時間を取得できるWin32APIのtimeGetTime関数を使う方が良いです。

MSDNに「計算では常に、2 つの timeGetTime 関数の戻り値の差を使います。」
とあるので素直に

timeGetTime() - timeGetTime()

とかして使いましょう。

MSDN→http://msdn.microsoft.com/ja-jp/library/cc428795.aspx

mmsystem.h と winmm.libの追加もいるみたいです。
----------------------------------------------


------------------以下ソース------------------

/* *  ・引数にint yama[136]を指定して直接書き込んでいます。 *  ・乱数には上述のようにメルセンヌツイスターを使用しています。 */
void genelate_yama(int yama[])
{
	int i,j=0,k=0;
	//元の山を生成
	for(i=1;i<38;i++){
		if(i%10==0) continue;
		while(j++<4) yama[k++]=i;
		j=0;
	} 
	//乱数の種
	init_genrand((unsigned)time(NULL)); 
	//山をシャッフル
	i=j=k=0;
	for(i=135;i>0;i--){
		j=genrand_int32()%i;
		k=yama[i];
		yama[i]=yama[j];
		yama[j]=k;
	}
}
//C++ STL のrandom_shuffle()を使った方が楽かも?
 

リーチ後の暗カン禁止 

ルールによりますが一般的には

1.送りカンでない
2.待ち不変
3.構成不変
4.役不変(九連のみ)

の4条件を見れば良い事になります。

1.送りカンについては、ツモった牌が4枚で無いかを見れば良いです。

2.待ち不変、3.構成不変については、構成不変は待ち不変を含むので、構成不変のみ見れば良いです。
シャンテン数と同様にバックトラック法でアンカンと仮定、仮定しないの2パターンについて
全構成を列挙し、同じかを見れば良いでしょう。

以上でほぼ全ての場合をチェックできます。

4.役不変(九連のみ)について、リーチなら当然メンゼン、テンパイという事になるので
「"牌姿"が九連のテンパイか」を見れば良いです(組み込むだけ無駄とも言える低確率ですが)。

------------------以下ソース------------------

書いたところ冗長になりすぎたので(…)クラス(C++)として公開します。
16/12/15 バグ修正


DL→riichigoankan.h(ヘッダ)
riichigoankan.cpp(本体、使用するソースと共にビルド)

使い方
1.riichigoankan.hをincludeする
2.RiichigoAnkanクラスのインスタンスを作る(RiichigoAnkan instなど)
3.bool check(int tehai[],int tumohai)メソッドでチェック。可能ならTRUE(bool値)を返す。

tehai[]はサイズ38以上int配列ポインタ渡し、赤、カン牌は統合済みである事。
tumohaiはカンする牌の牌番号


方針(得点計算) 

麻雀得点計算のプログラムは牌の表現方法などの「仕様」に強く依存しますので
全てのコードを掲載するのは難しいでしょう。
ここではプログラムの方針(あくまで一例)を示すに留めます。

・基本的には総当たり
基本的には全ての役を総当たりするしかありませんが
場合分けをする事で考慮すべき範囲が狭くなり、
さらに複合しない役を考慮することでステップ数を減らす事ができます。

?役の複合
・互いに複合しない役が成立した場合、複合しない方の判定を飛ばすことができます

(参考)
役の複合(Wikipedia)
ひいいの麻雀研究氏作役の複合表


一つの案として、スキップできる役のテーブルを作成するといいかもしれません。

1・#define PINFU 0など、役の定数を全て定義します。
2・bool yaku_skip[26]などの配列を作ります。これはTRUEならその役判定をスキップできるものとします。
3・複合しない役が多い順に役判定をしていきます。
4・役が成立したら「役の複合表」を参考に複合しない役に対応する配列にTRUEを代入します。(yaku_skip[TANYAO]=true; など)
5・以下、該当するテーブルがTRUEならその役判定をスキップできます。

・フーロ時に成立しない以下の役は飛ばすことができます
門前清模和
リーチ
一発
ダブルリーチ
平和
一盃口
二盃口
九蓮宝燈
九蓮宝燈9面待
四暗刻単騎
四暗刻


?何のあがりか(国士無双かチートイか通常手か)

・国士無双の場合

1・13面待か否かを見ます
2・点数を計算します


.チートイツの場合

1・役満か否か判定します。複合する可能性のある役満は以下の3つです
役満だった場合はそれ以上計算する必要がないので得点計算にうつります

天和
地和
字一色

2・役満でない場合は以下の役を判定します。
タンヤオ
清一色
混一色
混老頭
ハイテイ
ダブルリーチ
リーチ
一発
ツモ
ドラ

3・点数を計算します


.チートイツかつ通常手の場合

通常手のみ考えます。チートイツを考慮する必要はありません。
なぜならこの場合は常にリャンペイコウと複合するため、

リャンペイコウ(3飜)>チートイツ(2飜)

から、常に通常手の方が高くなるためです(高点法の原則(Wikipedia))。

.通常手の場合

1・まず流し満貫がどうか判定します
流し満貫だった場合はそれ以上計算する必要がないので得点計算にうつります

2・流し満貫でない場合は役満を判定します。
役満だった場合はそれ以上計算する必要がないので得点計算にうつります

3・役満でない場合はメンツ構成に関係無い(以下)役を判定します。

断ヤオ
飜牌
混老頭
小三元
混一色
清一色
リーチ
一発
門前清模和
槍槓
海底撈月
河底撈魚
嶺上開花
ダブルリーチ

4・メンツ構成に関係にある役(以下)と符を計算します。その中で役と符が最大になる組み合わせを採用します。
優先順位は 飜数>符 の順です。どちらも同じ場合は、高点法の原則からどの組み合わせでも良いので、最後に取りだした組み合わせを採用するなどして対応すれば良いでしょう。

平和
全帯
一盃口
三色同順
一気通貫
対々和
三色同刻
三暗刻
二盃口
純全帯
三槓子
四槓子


5・点数を計算します

当然ですがドラは単独で役にならないので、役がある場合のみ飜を加算します。


点数計算の方法

符と飜数がわかったら、支払う点数を求めます。
人間が得点計算をする時、通常は暗記した「早見表」と照らし合わせるという作業をしていますがここでは本来のやり方(?)で計算する方法を説明します。
この方がテーブルを作る作業が省けるので楽かもしれません。


参考)基本点の算出(Wikipedia)

? 基本点を求める

基本点=符×2^(飜数+2)
( Kihonten=Fu * pow(2.0 , fan + 2) など)

注)上限は2000点。2000以上は強制的に2000(満貫)に

ただし飜数が5以上ならば

・満貫(5飜)基本点=2000
・跳満(6~7飜)基本点=3000
・倍満(8~10飜)基本点=4000
・三倍満(11~12飜)基本点=6000
・役満(13飜以上or役満)基本点=8000
・n倍役満  基本点=8000×n

とする。

? 場合わけして算出

・子のロン和了   =基本点×4
・親のロン和了   =基本点×6
・親のツモ和了   =基本点×2
・子のツモ和了(親)=基本点×2
・子のツモ和了(子)=基本点×1


? 算出した点数から100未満を切り上げ

Tensuu=(Tensuu % 100 > 0) ? Tensuu + 100 - Tensuu % 100 : Tensuu; など

こうして出した点数にシバ棒やリーチ棒を含めて最終的な点数となります。


手牌のメンツ分解 

得点計算の際、符計算、メンツの構成が関係する役を判別するために手配が取り得るメンツ構成を列挙する必要があります。
メンツ構成のパターンを取りだしたら、そのパターンの符と役を計算し、全パターン取りだした時に最大のものを結果として“採用“します。
ここではシャンテン数計算と同じくバックトラック法を使用します。
ここでのメンツ分解の表現方法(仕様)は下記の通りです。

int kiriwake[10];

※n+1番目のメンツについて
kiriwake[n]=そのメンツのうち最も小さい牌番号
kiriwake[n+1]=メンツの種類

メンツの種類は

enum Mentu{
PON=0,
CHII,
ANKAN,
MINKAN,
ANKO,
SYUNTU,
TOITU
};

と定義しています。

結果をint kiriwake[10];に書き込んでいます。
注意するのは、この結果は一時的なものだという事です。「結果」を利用して符と役の計算をし、それを比較して「確定用」のデータに格納しなければ意味がありません。


------------------以下ソース------------------

//メンツ種類の列挙定数
enum Mentu{
	PON=0,
	CHII,
	ANKAN,
	MINKAN,
	ANKO,
	SYUNTU,
	TOITU
};
int kiriwake[10];//結果用グローバル変数
int p_kiriwake;  //kiriwake[10]のポインタ用変数
int agarihai;//あがり牌の番号
bool ron;//ロンあがりならTRUEになるフラグ
void KiriwakeNukidasi();
/***************************************************************************************/
void MentuKiriwake()
{
	int i;
	//ポインタ初期化
	p_kiriwake=0;
	//フーロ牌を統合
	//当HPの牌の表現方法、ルールについて参照
	for(i=0;i<=20;i+=4){
	if(fuuro[i]!=0){
		kiriwake[i]=fuuro[i];
		kiriwake[i+1]=fuuro[i+1];
		p_kiriwake+=2;
	}
	else{
		break;
		}
	}
	//頭抜き出し
	//あがった状態でしか呼び出さない点に注意
	for(i=0;i<=37;i++){
	for(;!tehai[i];i++);if(i>=38) continue;
		if(tehai[i] >=2){
			tehai[i] -=2;
		//データを格納しポインタを進める
		kiriwake[p_kiriwake]=TOITU;kiriwake[p_kiriwake+1] =i,p_kiriwake+=2;
		//メンツ抜き出し関数へ
		KiriwakeNukidasi();
		p_kiriwake-=2,kiriwake[p_kiriwake]=0,kiriwake[p_kiriwake+1]=0;
		tehai[i] += 2;
		}
	}
}
/***************************************************************************************/
//メンツ抜き出し関数
void KiriwakeNukidasi()
{
	int i;
	for(i=0;i<38;i++){
	for(;!tehai[i];i++);
	if(i>=38){
		if(kiriwake[9]!=0){//4メンツ1頭とれてるなら
		/********************************/
		/* ここで切り分けが終わります  */
		/* ここから符計算等に入ります  */
		/* 構成が関係する役はメンツごと */
		/* にここで判定すること     */
		/********************************/
		}
	return;
	}
	//アンコ抜き出し
	if(tehai[i] >=3){
		tehai[i] -=3;
		//ロンのあがり牌ならミンコウである
		if(i==agarihai&& ron==true){
			kiriwake[p_kiriwake]=PON;
		}
		else{
			kiriwake[p_kiriwake]=ANKO;
		}
		kiriwake[p_kiriwake+1] =i,p_kiriwake+=2;
		KiriwakeNukidasi();//再帰
		p_kiriwake-=2,kiriwake[p_kiriwake]=0,kiriwake[p_kiriwake+1]=0;
		tehai[i] += 3;
	}
	//シュンツ抜き出し
	if(tehai[i] && tehai[i+1] && tehai[i+2] &&i<30){
		tehai[i]--,tehai[i+1]--,tehai[i+2]--;
		kiriwake[p_kiriwake]=SYUNTU,kiriwake[p_kiriwake+1] =i,p_kiriwake+=2;
 		KiriwakeNukidasi();//再帰
		p_kiriwake-=2,kiriwake[p_kiriwake]=0,kiriwake[p_kiriwake+1]=0;
		tehai[i]++,tehai[i+1]++,tehai[i+2]++;
		}	            
	}
}

符の計算 

・メンツ分けが済んでいる前提です
・全てのメンツ構成に対し再帰的に何度もこの関数を呼ぶ事が前提です



参考)符の計算(Wikipedia)
1・副底・・・必ず20符
2・門前加符・・・メンゼンロンで+10符(メンゼン、ロンあがりの情報が必要)
3・ツモ符・・・平和以外でツモ和了した場合+2符(ピンフかどうかの情報が必要)
4・待ちによる符・・・ペンチャン、カンチャン、単騎待ちで+2符(あがり牌の情報が必要)
5・雀頭・・・役牌の場合+2符、連風牌ならさらに+2符(自風、場風の情報が必要)
6・面子の構成による符

  チュウチャン ヤオチュウ
ミンコウ 2符 4符
アンコウ 4符 8符
ミンカン 8符 16符
アンカン 16符 32符


7・鳴き平和形かどうか・・・鳴き平和形なら30符固定

を順に計算していけば良いでしょう。チートイツは25符固定です。手配以外にそれぞれ必要な情報があります。

------------------以下ソース------------------

/* * グローバル変数int fu に結果を格納しています */
int fu=0;//結果用
bool menzen;//メンゼンフラグ(TRUE=メンゼン)
bool ron;//ロンフラグ(TRUE=ロン  FALSE=ツモ)
bool is_pinfu;//そのメンツ構成でピンフかどうか(TRUE=ピンフ)
int agarihai;//あがり牌の牌番号
void CalcFu()
{
	//0・ピンフなら
	if(is_pinfu){
		if(!ron)
			fu_w=20;
		else
			fu_w=30;
		//結果用fuと比較し作業用の方が大きいなら記憶
		if (fu <fu_w) fu=fu_w;
		return;
	}
	int fu_w=20;//作業用の符、1・副底を代入
	//2・門前加符
	if(menzen & ron)  fu_w += 10;
	//3・ツモ符
	if(!ron )  fu_w+=2;
	//4・待ち
	for(int i=0;i<10;i+= 2){
	if(kiriwake[i]==TOITU){
	//単騎待ち
	if(kiriwake[i+1]==agarihai){
		fu_w += 2;
		break;
		}
	}
	else if(kiriwake[i]==SYUNTU ){
	if(
		(kiriwake[i+1]==agarihai-2 &&kiriwake[i+1]%10==1) ||//ペン3待ち
		(kiriwake[i+1]==agarihai &&kiriwake[i+1]%10==7) ||//ペン7待ち
		kiriwake[i+1]==agarihai-1||//カンチャン
		kiriwake[i+1]==agarihai+1//カンチャン
		)
		{
			fu_w += 2;
			break;
			}
		}
	}
	//5・雀頭
	for(int i=0;i<10;i+= 2){
	if(kiriwake[i]==TOITU){
		if(kiriwake[i+1] >=35||kiriwake[i+1]==bakaze)//役牌
			fu_w += 2;
		if(kiriwake[i+1]==jikaze)//連風牌
		 fu_w += 2;
		}
	}
	//6・面子の構成による符
	for(int i=0; i<10 ;i+= 2){
		if(kiriwake[i]==ANKO){//暗コウ
			if(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9||kiriwake[i+1] > 30)
				fu_w += 8;//ヤオチュウ
			else
				fu_w += 4;// 中張
		}
		else if(kiriwake[i] ==PON){	//明刻
			if(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9||kiriwake[i+1] > 30)
				fu_w += 4;//ヤオチュウ
			else
				fu_w += 2;// 中張
		}
		else if(kiriwake[i]==ANKAN){ //アンカン
			if(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9||kiriwake[i+1] > 30)
				fu_w += 32;//ヤオチュウ
			else
				fu_w += 16;// 中張
		}
		else if(kiriwake[i]==MINKAN){	    //ミンカン
			if(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9||kiriwake[i+1] > 30)
				fu_w += 16;//ヤオチュウ
			else
				fu_w += 8;// 中張
		}
	} 
	//7・鳴きピンフ形
	//ここまでで加符が無い=20符=鳴きピンフ形
	if(fu_w==20) fu_w=30;
	//1の位切り上げ
	fu_w=(fu_w % 10 > 0)  ?  fu_w + 10 - fu_w % 10 : fu_w;
	//結果用fuと比較し作業用の方が大きいなら記憶
	if (fu <fu_w) fu=fu_w;
}

国士無双13面待 

シャンテン数の計算で国士無双のあがりがわかっているものとします
後は単純に手配からあがり牌を引いて、全種類1枚かどうかを見れば良いでしょう。

------------------以下ソース------------------

/* * 国士無双13面待ならTRUEを返します */ 
int agarihai;//あがり牌が入っているグローバル変数とします
bool Kokusi13men()
{
	int specie[13]={1,9,11,19,21,29,31,32,33,34,35,36,37};
	int i=0;
	tehai[agarihai]--;
	for(;i<13;i++){
		if(tehai[specie[i]]!=1){
			tehai[agarihai]++;
			return false;
		}
	}
	tehai[agarihai]++;
	return true;
}
 

九蓮宝燈、九蓮宝燈9面待 

条件
・鳴いていないとします
・あがり判定は既に済んでいるとします
・九蓮宝燈9面待なら九蓮宝燈を判定する必要はありません 
九蓮宝燈はまず1マンか1ソウか1ピンがあるかどうか見ます。無しor2種類以上あるならFALSEを返します。
ある場合は種類を記憶し、同種の数牌を1から9まで
1、9  →	3枚以上あるか
2~8  →	1枚以上あるか
を見ます。
九蓮宝燈9面待は手配からあがり牌を引いて
構成が「311111113」(n1~n9の枚数、nは種類、n<3)かつ
牌種が全て同じかどうかを見れば良いでしょう。 
天和、地和以外とは複合しませんので、TRUEなら他の役を考慮する必要はありません

------------------以下ソース------------------

/* * 九蓮宝燈ならTRUEを返します */
bool Tyuuren()
{
	int syurui=0;
	int i;
	if((bool)tehai[1] + (bool)tehai[11] + (bool)tehai[21] != 1 )//C言語でTRUEは数字の1として扱える
		return false;
	//字牌があれば当然FALSE
	for(i=31;i<=37;i++){
		if(tehai[i]) return false;
	}
	//種類を記憶
	if(tehai[1])		syurui=0;
	else if(tehai[11])	syurui=1;
	else if(tehai[21])	syurui=2;
	for(i=syurui*10+1;i<=syurui*10+9;i++){
	switch(i%10){
		case 1:
		case 9:
		if(tehai[i]<3)
			return false;
		break;
		default:
		if(tehai[i]<1)
			return false;
		break;
		}
	}
	return true;
}
/***************************************************************************************/
/* * 九蓮宝燈9面待ならTRUEを返します */
int agarihai;//あがり牌がはいっているグローバル変数
bool Tyuuren9()
{
	int syurui=0;
	int i;
	if((bool)tehai[1] + (bool)tehai[11] + (bool)tehai[21] != 1 )   //C言語でTRUEは数字の1として扱える
		return false;
	//字牌があれば当然FALSE
	for(i=31;i<=37;i++){
		if(tehai[i]) return false;
	}
	//種類を記憶
	if(tehai[1])		syurui=0;
	else if(tehai[11])	syurui=1;
	else if(tehai[21])	syurui=2;
	tehai[agarihai]--;
	for(i=syurui*10+1;i<=syurui*10+9;i++){
	switch(i%10){
		case 1:
		case 9:
		if(tehai[i]!=3){
			tehai[agarihai]++;
			return false;
		}
		break;
		default:
		if(tehai[i]!=1){
			tehai[agarihai]++;
			return false;
		}
		break;
		}
	}
	tehai[agarihai]++;
	return true;
}

 

大四喜和 

東、南、西、北がそれぞれ3枚ずつあればTRUEを返します。
------------------以下ソース------------------

/* * 大四喜和ならTRUEを返します */
bool Daisuusii()
{
	for(int i=31;i<=34;i++){
		if(tehai[i] <3)
			return false;
	}
	return true;
}

 

四暗刻、四暗刻単騎 

単純に手配の構成がコーツ4つ+トイツ1つかどうかを見れば良いだけですが
コーツが、アンコかミンコか判断するには、それ用の情報が必要です。
ここでは鳴いていたら(アンカンは含まず)TRUEになる「boolis_naki」というフラグで判別しています。
四暗刻単騎待ちは、手配からあがり牌を引いて構成が「アンコ×4+孤立牌(頭)」かどうかを見れば良いでしょう。
メンツの構成に依存する役ですが、役満なので考慮しなくても良いです。


------------------以下ソース------------------

bool is_naki;//鳴いていたらTRUEになるフラグ(アンカンは含まず)。アンカンは3枚(コーツ)としています
int agarihai;//あがり牌の番号が入っているグローバル変数
int Is_ron;//ロンあがりならTRUEになるフラグ(シャボ待ち四暗刻はツモのみでしか成立しないため)
/* * 四暗刻ならTRUEを返します */
bool Suuankou()
{
	int i,kootu=0;
	bool toitu=false; 
	//鳴いていたらorロンあがりならFALSE
	if(Is_naki || Is_ron)
		return false;
	for(i=0;i<=37;i++){
		for(;!tehai[i];i++); if(i>=38) continue;
		if(tehai[i]==1)
			return false;
		else if(toitu==false && tehai[i]==2)
			toitu=true ;
		else if(tehai[i]==3)
			kootu++;
	}
	return (kootu==4 && toitu) ? true : false;
}
/***************************************************************************************/
/* * 四暗刻単騎ならTRUEを返します */
bool SuuankouTanki()
{
	int i,kootu=0;
	bool toitu=false;
	//鳴いていたらFALSE
	if(Is_naki)
		return false;
	tehai[agarihai]--;
	for(i=0;i<=37;i++){
		for(;!tehai[i];i++); if(i>=38) continue;
		 
		if(tehai[i]==1 && (i==agarihai))
			toitu=true;
		else if(tehai[i]==3)
			kootu++;
		}
	}
	tehai[agarihai]++;
	return (kootu==4 && toitu) ? true : false;
}

 

字一色 

字牌以外がなければTRUEを返します。

------------------以下ソース------------------

/* * 字一色ならTRUEを返します */
bool Tuuiisou()
{
	int i=0;
	for(;i<=37;i++){ 
		for(;!tehai[i];i++); if(i>=38) continue;
		if(i<30)
			return false;
	}
	return true;
}

緑一色 

手牌に2,3,4,6,8ソウ、発以外がなければTRUEを返します

------------------以下ソース------------------

/* * 緑一色ならTRUEを返します */
bool Ryuuiisou()
{
	int i=0;
	for(;i<=37;i++){ 
		for(;!tehai[i];i++);	if(i>=38) continue;
		if(!(i==22||i==23||i==24||i==26||i==28||i==36))
			return false;
	}
	return true;
}

 

小四喜和 

大四喜と複合しないので大四喜でない時に呼びます。大四喜のプログラムに、1種がトイツかどうかを見れば良いでしょう。
------------------以下ソース------------------

/* * 小四喜和ならTRUEを返します */
bool Syousuusii()
{
	bool toitu=false;
	int anko=0;
	for(int i=31;i<=34;i++){
		if(toitu==false && tehai[i]==2)
			toitu=true ;
		else if(tehai[i]==3)
			anko++;
	}
	return (anko==3 && toitu) ? true : false;
}
 

大三元 

白、発、中が3枚ずつあればTRUEを返します。

------------------以下ソース------------------

/* * 大三元ならTRUEを返します */
bool Daisangen()
{
	if(tehai[35]==3 && tehai[36]==3 && tehai[37]==3)
		return true;
	return false;
}
 

清老頭 

老頭牌以外がなければTRUEを返します。

------------------以下ソース------------------

/* * 清老頭ならTRUEを返します */
bool Tinroutou()
{
	int i=0;
	for(;i<=37;i++){ 
		for(;!tehai[i];i++); if(i>=38) continue;
		if(!(i==1||i==9||i==11||i==19||i==21||i==29))
			return false;
	}
	return true;
}

平和 

メンツ分けが済んでいる前提です

条件の
1・メンゼン
2・頭が役牌でない
3・両面待ち
4・シュンツ×4
をそれぞれ見ます。

手配以外に
・あがり牌
・自風、場風
の情報が必要です
------------------以下ソース------------------

/* * 平和ならTRUEを返します */
int agarihai;	//あがり牌
int bakaze;	//場風
int jikaze;	//自風
bool Pinfu()
{
	//頭が役牌でないか、シュンツ以外はないか
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==TOITU){
			if(kiriwake[i+1]==jikaze ||kiriwake[i+1]==bakaze ||kiriwake[i+1] >=35 )
				return false;
		}
		else if(kiriwake[i] != SYUNTU)
			return false;
	}
	//両面待ち
	bool ryanmen=false;    
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==SYUNTU ){
			if((kiriwake[i+1]==agarihai-2 &&kiriwake[i+1]%10 >=2 )||
		(kiriwake[i+1]==agarihai  &&kiriwake[i+1]%10 < 7 ) )
		 ryanmen=true;
		}
	} 
	return ryanmen ? true :false;
}

 

タンヤオ 

手牌にヤオチュウ牌がなければTRUEを返します。
------------------以下ソース------------------

/* * タンヤオならTRUEを返します */
bool Tanyao()
{
	int i=0;
	for(;i<=37;i++){ 
		for(;!tehai[i];i++);	if(i>=38) continue;
		if(i%10==1||i%10==9||i>30)
		return false;
	}
	return true;
}

 

一盃口 

メンツ分けが済んでいる前提です
メンゼンかつ同じシュンツが2つ以上あるか見ます。

------------------以下ソース------------------

/* * 一ペイコウならTRUEを返します */
bool Iipeikou()
{
	int chk[38];//作業用
	memset(chk,0,sizeof(chk));
	for(int i=2;i<10;i+= 2){
		if(kiriwake[i]==SYUNTU ){
	        chk[kiriwake[i+1]]++;
		}
	}
	for(int i=0;i<=37;i++){
	for(;!chk [i];i++);if(i>=38) continue;
		if(chk[i] >=2){
			return true;
		}
	}
	return false;
}

飜牌(白、発、中) 

白、発、中がそれぞれ3枚あればTRUEを返します。いちいち呼ぶのが面倒なら、共通の飜数用の変数を用意して値を加算する方法もあります
------------------以下ソース------------------

/* * 白ならTRUEを返します */
bool Haku ()
{
	if(tehai[35]==3)
		return true;
	return false;
}
/***************************************************************************************/ 
/* * 発ならTRUEを返します */
bool Hatu ()
{
	if(tehai[36]==3)
		return true;
	 
	return false;
}
/***************************************************************************************/
/* * 中ならTRUEを返します */
bool Tyun ()
{
	if(tehai[37]==3)
		return true;
	 
	return false;
}
 
 

全帯 

メンツ分けが済んでいる前提です
メンツに123,789.111,999以外がなく頭がヤオチュウ牌かつ字牌があるかを見れば良いです

------------------以下ソース------------------

/* * チャンタならTRUEを返します */
bool Tyanta()
{
	bool jihai=false;//字牌フラグ
	for(int i=0;i<10;i+= 2){
		//頭
		if(kiriwake[i]==TOITU){
			if(! (kiriwake[i+1]%10== 1||kiriwake[i+1]%10== 9) &&kiriwake[i+1] <30 )
				return false;
			if(kiriwake[i+1]  > 30)
				jihai=true;
		}
		//シュンツ
		else if(kiriwake[i]==SYUNTU ||kiriwake[i]==CHII){
			if(!(kiriwake[i+1]%10==1||kiriwake[i+1]%10==7) )
				return false;
		}
		//コーツ
		else if (kiriwake[i]==ANKO ||kiriwake[i]==ANKAN ||kiriwake[i]==MINKAN)){
			if(!(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9  ||kiriwake[i+1]  > 30))
				return false;
			if(kiriwake[i+1]  > 30)
				jihai=true;
		}
	}
	return jihai? true : false;
}

 

三色同順 

メンツ分けが済んでいる前提です
単に同じ数、非同色のシュンツが3つあるか見れば良いです。

------------------以下ソース------------------

 
/* * 三色同順ならTRUEを返します */
bool SansyokuDoujyun()
{
	int chk[30];       //作業用
	memset(chk,0,sizeof(chk));
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==SYUNTU ||kiriwake[i]==CHII){
			chk[kiriwake[i+1]]++;
		}
	}
	for(int i=1; i<10;i++)
		if(chk[i]&&chk[i+10]&&chk[i+20])
			return true;
	return false;
}
 

一通 

メンツ分けが済んでいる前提です
同色の123,456,789のシュンツがあるか見れば良いです。

------------------以下ソース------------------

/* * 一通ならTRUEを返します */
bool Ittuu()
{
	bool chk[9];//作業用
	memset(chk,0,sizeof(chk));
	for(int i=0;i<10;i+= 2){
	if(kiriwake[i]==SYUNTU ||kiriwake[i]==CHII){
		if(kiriwake[i+1]==1 ) chk[0]=1;
		else if(kiriwake[i+1]==4 ) chk[1]=1;
		else if(kiriwake[i+1]==7 ) chk[2]=1;
		else if(kiriwake[i+1]==11) chk[3]=1;
		else if(kiriwake[i+1]==14) chk[4]=1;
		else if(kiriwake[i+1]==17) chk[5]=1;
		else if(kiriwake[i+1]==21) chk[6]=1;
		else if(kiriwake[i+1]==24) chk[7]=1;
		else if(kiriwake[i+1]==27) chk[8]=1;
		}
	}
	if((chk[0]& chk[1] & chk[2])|| (chk[3] & chk[4]& chk[5]) || (chk[6]& chk[7] & chk[8]))
	{
		return true;
	}
	return false;
}

トイトイ 

メンツ分けが済んでいる前提です
メンツにコーツ、カンツ以外が無ければTRUEをかえします。

------------------以下ソース------------------

/* * トイトイならTRUEを返します */
bool Toitoi()
{
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==SYUNTU ||kiriwake[i]==CHII)
			return false;
	}
	return true;
}
 

三色同刻 

メンツ分けが済んでいる前提です
単に同じ数、非同色のコーツが3つあるか見れば良いです。

------------------以下ソース------------------

/* * 三色同刻ならTRUEを返します */
bool SansyokuDoupon()
{
	int chk[50];       //作業用
	memset(chk,0,sizeof(chk));
	for(int i=0;i<10;i+= 2){
		if (kiriwake[i]==PON ||kiriwake[i]==ANKO ||kiriwake[i]==ANKAN ||kiriwake[i]==MINKAN){
			chk[kiriwake[i+1]]++;
		}
	}
	for(int i=1; i<10;i++)
		if(chk[i]&&chk[i+10]&&chk[i+20])
			return true;
	return false;
}
 

混老頭 

手牌に老頭牌と字牌以外がなく老頭牌と字牌どちらも存在する場合にTRUEを返します。

------------------以下ソース------------------

/* * 混老頭ならTRUEを返します */
bool Honroutou()
{
	int i=0;
	bool jihai=false;
	bool routou=false;
	for(;i<=37;i++){ 
		for(;!tehai[i];i++); if(i>=38) continue;
		if((i%10 > 1 &&i%10 <9) &&i<30 )    //老頭or字牌以外ならFALSE
			return false;
			else if((i%10==1||i%10==9) &&i<30 && routou==false) //老頭牌を記憶
				routou=true;
		else if(i> 30 && jihai==false) //字牌を記憶
			jihai=true ;
	}
	return (routou & jihai) ? true : false;
}

三暗刻 

メンツ分けが済んでいる前提です
アンコかアンカンが3つあるかを見ます。

------------------以下ソース------------------

/* * 三アンコウならTRUEを返します */
bool Sanankou()
{
	int chk=0;
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==ANKO ||kiriwake[i]==ANKAN){
			chk++;
		}	            
	}
	return (chk >=3) ? true : false;
}

小三元 

白、発、中のうち2種がコーツ、1種がトイツであればTRUEを返します。

------------------以下ソース------------------

  /* * 小三元ならTRUEを返します */
bool Syousangen()
{
	bool toitu=false;
	int koutu=0;	int i; 	for(i=35;i<=37;i++){ 		if(tehai[i]==2 && toitu==false) 			toitu=true; 		if(tehai[i]==3 ) 			koutu++; 	} 	return (koutu==2 && toitu==true) ? true : false; }   

二盃口 

メンツ分けが済んでいる前提です
メンゼンかつ同じシュンツが2つ以上、2種類あるか見ます。
注:1111222233334444は二盃口です。

------------------以下ソース------------------

/* * 二ペイコウならTRUEを返します */
bool Ryanpeikou()
{
	int chk[38],kazu=0;//作業用
	memset(chk,0,sizeof(chk));
	for(int i=0;i<10;i+= 2){
		if(kiriwake[i]==SYUNTU ){
			chk[kiriwake[i+1]]++;
		}
	} 
	for(int i=0;i<=37;i++){
		for(;!chk[i];i++);if(i>=38) continue;
		if(chk[i] >=2){
		if(++kazu==2|| chk[i]==4)
			return true;
		}
	}
	return false;
}
 

混一色 

清一色と複合しないので清一色でない時に呼ぶようにします。清一色判定の処理に字牌が含まれるかどうかを見ればよいでしょう。

------------------以下ソース------------------

/* * 混一色ならTRUEを返します */
bool Honitu()
{
	int syurui;
	int i;
	bool jihai=false;
	//最初の牌種を記憶
	for(i=0;!tehai[i];i++);
		syurui=i/10;
	if(syurui==3) return false;//最初の牌種が字牌ならFALSE
	//残りの牌<>最初の牌種ならFALSE
	//字牌があれば記憶
	for(;i<=37;i++){
		for(;!tehai[i];i++);if(i>=38) continue;
		if(i/10 != syurui&&i<30 )
			return false;
		else if(i>30 && !jihai)
			jihai=true;
	}
	return jihai? true : false;
}
 

純チャンタ 

メンツ分けが済んでいる前提です
メンツに123,789.111,999以外がなく頭が老頭牌であるかを見れば良いです

------------------以下ソース------------------

/* * 純チャンタならTRUEを返します */
bool JyunTyanta()
{
	for(int i=0;i<10;i+= 2){
		//頭
		if(kiriwake[i]==TOITU){
			if(!(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9 ) ||kiriwake[i+1]  > 30 )
				return false;
		}
		//シュンツ
		else if(kiriwake[i]==SYUNTU ||kiriwake[i]==CHII){
			if(!(kiriwake[i+1]%10==1||kiriwake[i+1]%10==7) )
				return false;
		}
		//コーツ
		else if (kiriwake[i]==ANKO ||kiriwake[i]==ANKAN ||kiriwake[i]==MINKAN) {
			if(!(kiriwake[i+1]%10==1||kiriwake[i+1]%10==9 ) ||kiriwake[i+1]  > 30)
				return false;
		}
	}
	return true;
}
 

清一色 


ところでC言語には便利なforeachがありません。このHPでは下記のコードで代用しています。
for(inti=0;i<=37;i++){
for(;!tehai[i];i++);
if(i>=38) continue;
/*  処理  */
}
参考)とつげき東北氏HP

清一色判定処理は
1・上記コードで最初に引っかかった牌の種類を記憶します。
ex) syurui=syurui/10;//0.マンズ 1.ピンズ 2.ソーズ 3.字牌
2.残りの牌種を走査して、記憶した種類と違うならその時点でFALSEをかえします

ちなみにif(i>=38) continue;の部分で中身が0で無い限りtehai[38]以降のアドレスを検索してしまい無駄ですので
tehaiを1つ拡張し最後尾に番兵(-1など)を入れておくと処理時間を稼げます。


------------------以下ソース------------------

/* * 清一色ならTRUEを返します */
bool Tinitu()
{
	int syurui;
	int i;
	//最初の牌種を記憶
	for(i=0;!tehai[i];i++);
	syurui=i/10;
	if(syurui==3) return false;//字牌ならFALSE 
	//最初の牌種と違うならFALSE
	for(;i<=37;i++){
	for(;!tehai[i];i++);if(i>=38) continue;
		if(i/10 != syurui)
		return false;
	}
	return true;
}


後書

・ソースをHTMLに変換する
Code2Htmlを使用しています
抽選王的なやつ(暇つぶしで作ったJavaScript) 

・管理人メール→cmj33あっとexcite.co.jp

 

inserted by FC2 system