麻雀AIをmrubyで書こうとして撃沈した話

# やろうとしたこと
自分がやってる麻雀AIをmrubyで書こうとしました。

https://osdn.jp/projects/openmahjong/

今まではC言語で書いていたのですが、読み返すと訳わからないですし、たぶんどこかバグがあると思うのですが、それを追跡することが不可能に思えています。rubyで書けばコレクション操作を直感的で簡潔に記述できますし、malloc、freeがーとか配列のインデックスがーとかから解放されますのでコーディングのストレス解消につながると考えました。
ただ、いきなりAIを全部書き出すのは撃沈リスクがあったので、まずはAIの一部で使っている山にある牌をシミュレーションで読むメソッド(MJ0)をmrubyで書いて実験することにしました。



# ソースコード
## C言語
```C
int MJ0(/* inputs */
		MJ0PARAM *pParam,int *pDora,int dlength,
		/* outputs */
		double *pNokorihai,double *pKikenhai,
		double *pMentsuSimo,double *pMentsuToimen,double *pMentsuKami)
{
	int i,j,k;
	int sum;
	int index;
	int mindex;
	int pai;
	int furiten;
	unsigned int r;
	int aNokorihai[34];
	int iniMentsu[3];
	int aPaikukan[4][34];
	int curMentsu[3];
	MJ0PARAM *pCurPrm;
	MJ0_COMBI aCombi[21+34];
	int aAnpai[3][34];
	int aRiichi[3];
	int aMentsu[3][5];
	int aMachi[34];

	iniMentsu[0] = 0;
	iniMentsu[1] = 0;
	iniMentsu[2] = 0;
	aRiichi[0] = 0;
	aRiichi[1] = 0;
	aRiichi[2] = 0;

	memset(aAnpai,0,sizeof(aAnpai));

	for(i=0;i<34;i++){
		pNokorihai[i] = 0.0;
		pKikenhai[i] = 0.0;
		aNokorihai[i] = 4;
	}

	for(i=0;i<21+34;i++){
		pMentsuSimo[i] = 0.0;
		pMentsuToimen[i] = 0.0;
		pMentsuKami[i] = 0.0;
	}

	/* 残り牌を初期化 */
	pCurPrm = pParam;
	for(j=0;j<pCurPrm->pTehai->tehai_max;j++){
		aNokorihai[pCurPrm->pTehai->tehai[j] & 63]--;
	}
	for(i=0;i<4;i++){

		for(j=0;j<pCurPrm->pTehai->minshun_max;j++){
			aNokorihai[pCurPrm->pTehai->minshun[j] & 63]--;
			aNokorihai[(pCurPrm->pTehai->minshun[j] & 63) + 1]--;
			aNokorihai[(pCurPrm->pTehai->minshun[j] & 63) + 2]--;
		}
		if(i!=0) iniMentsu[i-1]+= pCurPrm->pTehai->minshun_max;

		for(j=0;j<pCurPrm->pTehai->minkou_max;j++){
			aNokorihai[pCurPrm->pTehai->minkou[j] & 63]-=3;
		}
		if(i!=0) iniMentsu[i-1]+= pCurPrm->pTehai->minkou_max;
		
		for(j=0;j<pCurPrm->pTehai->minkan_max;j++){
			aNokorihai[pCurPrm->pTehai->minkan[j] & 63]-=4;
		}
		if(i!=0) iniMentsu[i-1]+= pCurPrm->pTehai->minkan_max;
		
		for(j=0;j<pCurPrm->pTehai->ankan_max;j++){
			aNokorihai[pCurPrm->pTehai->ankan[j] & 63]-=4;
		}
		if(i!=0) iniMentsu[i-1]+= pCurPrm->pTehai->ankan_max;

		/* 鳴かれた牌は既に勘定しているので除く */
		for(j=0;j<pCurPrm->kawalength;j++){
			if(!(pCurPrm->pKawahai[j].state & MJKS_NAKI)) aNokorihai[pCurPrm->pKawahai[j].hai & 63]--;
			/* 現物を安牌にする */
			if(i!=0){
				aAnpai[i-1][pCurPrm->pKawahai[j].hai & 63] = 1;
			}

		}

		/* もし既に固定しておく面子があるのであればそれをここで行う(TODO) */

		pCurPrm++;
	}

	/* リーチ後の捨て牌を安パイにする(捨て順を考慮してないけど) */
	i = 0;
	j = 0;
	while(1){
		if(j >= pParam[i].kawalength) break;
		if(i!=0){
			if(pParam[i].pKawahai[j].state & MJKS_REACH){
				aRiichi[i - 1] = 1;
			}
			for(k=1;k<4;k++){
				if(aRiichi[k - 1]){
					aAnpai[k - 1][pParam[i].pKawahai[j].hai & 63] = 1;
				}
			}
		}

		i = (i + 1) & 3;
		if(i==0) j++;
	}

	for(i=0;i<dlength;i++){
		int dora = pDora[i] & 63;
		/* ドラ表示牌を取得 */
		if (dora < 27){
			if ((dora % 9) == 0) {
				dora += 8;
			}
			else{
				dora--;
			}
		}
		else if (dora == 27){
			dora = 30;
		}
		else if (dora == 31){
			dora = 33;
		}
		else{
			dora--;
		}
		aNokorihai[dora]--;
	}


	/* 指定回数分シミュレーションを繰り返す */
	for(i=0;i<SIMU_SIZE;i++){
		for(j=0;j<4;j++){
			memcpy(&aPaikukan[j][0],aNokorihai,sizeof(aNokorihai));
		}

		memset(aMentsu,0xFF,sizeof(aMentsu));
		memcpy(curMentsu,iniMentsu,sizeof(iniMentsu));

		/* もし、一色系などを読むのであればここで牌空間を変更しておくと良い(TODO) */


		/* 4面子全ての相手が揃うまで繰り返す */
		while(curMentsu[0] != 4 || curMentsu[1] != 4 || curMentsu[2] != 4){
			for(j=0;j<3;j++){
				if(curMentsu[j] == 4) continue;
				/* 組み合わせ数の初期化 */

				/* 順子の組み合わせ */
				for(k=0;k<21;k++){
					pai = (k / 7) * 9 + k % 7; // 計算する牌番号の先頭の値
					aCombi[k].num = aPaikukan[j][pai] * aPaikukan[j][pai+1] * aPaikukan[j][pai+2];
				}

				/* 刻子の組み合わせ */
				for(k=0;k<34;k++){
					if(aPaikukan[j][k-21] == 4){
						aCombi[k+21].num = 4;
					}else if(aPaikukan[j][k-21] == 3){
						aCombi[k+21].num = 1;
					}else{
						aCombi[k+21].num = 0;
					}
				}

				/* 待ち読みによる組み合わせ数の更新(TODO) */

				/* 合計を計算 */
				sum = 0;
				for(k=0;k<21+34;k++){
					aCombi[k].sum = sum;
					sum += aCombi[k].num;
				}

				/* 乱数により面子を生成 */
				if(sum == 0) goto failure;
				r = ((unsigned int)rand()) % sum;

				/* そんなにたくさんないからシーケンシャルサーチで */
				for(k=0;k<21+34;k++){
					if(r < aCombi[k].sum + aCombi[k].num && aCombi[k].num != 0){
						/* この面子に決定！ */
						aMentsu[j][curMentsu[j]] = k;

						/* 面子を構成する牌を牌空間から除去 */
						if(k<21){
							if(aPaikukan[0][k] > 0)   aPaikukan[0][k]--;
							if(aPaikukan[0][k+1] > 0) aPaikukan[0][k+1]--;
							if(aPaikukan[0][k+2] > 0) aPaikukan[0][k+2]--;
							if(aPaikukan[1][k] > 0)   aPaikukan[1][k]--;
							if(aPaikukan[1][k+1] > 0) aPaikukan[1][k+1]--;
							if(aPaikukan[1][k+2] > 0) aPaikukan[1][k+2]--;
							if(aPaikukan[2][k] > 0)   aPaikukan[2][k]--;
							if(aPaikukan[2][k+1] > 0) aPaikukan[2][k+1]--;
							if(aPaikukan[2][k+2] > 0) aPaikukan[2][k+2]--;
							if(aPaikukan[3][k] > 0)   aPaikukan[3][k]--;
							if(aPaikukan[3][k+1] > 0) aPaikukan[3][k+1]--;
							if(aPaikukan[3][k+2] > 0) aPaikukan[3][k+2]--;
						}else{
							if(aPaikukan[0][k-21] >= 3) aPaikukan[0][k-21]-=3;
							if(aPaikukan[1][k-21] >= 3) aPaikukan[1][k-21]-=3;
							if(aPaikukan[2][k-21] >= 3) aPaikukan[2][k-21]-=3;
							if(aPaikukan[3][k-21] >= 3) aPaikukan[3][k-21]-=3;
						}

						if(j==0){
							pMentsuSimo[k] += 1.0;
						}else if(j==1){
							pMentsuToimen[k] += 1.0;
						}else{
							pMentsuKami[k] += 1.0;
						}

						curMentsu[j]++;
						break;
					}
				}

			}
			
		}

		/* アタマの生成 */
		for(j=0;j<3;j++){
			for(k=0;k<34;k++){
				if(aPaikukan[j][k] == 4){
					aCombi[k].num = 6;  // 4 C 2
				}else if(aPaikukan[j][k] == 3){
					aCombi[k].num = 3;  // 3 C 2
				}else if(aPaikukan[j][k] == 2){
					aCombi[k].num = 1;  // 2 C 2
				}else{
					aCombi[k].num = 0;
				}
			}
			/* 待ち読みによる組み合わせ数の更新(TODO) */

			/* 合計を計算 */
			sum = 0;
			for(k=0;k<34;k++){
				aCombi[k].sum = sum;
				sum += aCombi[k].num;
			}

			/* 乱数により対子を生成 */
			if(sum == 0) goto failure;
			r = ((unsigned int)rand()) % sum;
			/* そんなにたくさんないからシーケンシャルサーチで */
			for(k=0;k<34;k++){
				if(r < aCombi[k].sum + aCombi[k].num && aCombi[k].num != 0){
					/* この牌に決定！ */
					aMentsu[j][4] = k;
					/* アタマを構成する牌を牌空間から除去 */
					if(aPaikukan[0][k] >= 2) aPaikukan[0][k]-=2;
					if(aPaikukan[1][k] >= 2) aPaikukan[1][k]-=2;
					if(aPaikukan[2][k] >= 2) aPaikukan[2][k]-=2;
					if(aPaikukan[3][k] >= 2) aPaikukan[3][k]-=2;
					break;
				}
			}
		}

		

		/* 当たり牌を決定する */
		for(;;){
			memset(aMachi,0,sizeof(aMachi));

			j = rand() % 3; // 誰の面子を当たりにするか
			mindex = rand() % ((4 - iniMentsu[j]) * 3 + 2); // どの牌を当たり牌にするか
			if(mindex >= (4 - iniMentsu[j]) * 3){
				/* 単騎待ち */
				assert(aMentsu[j][4] >= 0 && aMentsu[j][4] < 34);
				aMachi[aMentsu[j][4]] = 1;
          

				if(aMentsu[j][4] < 27){
					/* 延べ単騎 */
					for(k=0;k<4;k++){
						if(aMentsu[j][k] >= 0 && (aMentsu[j][k] % 7) != 6 && 
							(aMentsu[j][k] / 7)*9 + (aMentsu[j][k] % 7) + 3 == aMentsu[j][4]){
							assert(aMentsu[j][4] >= 3);
							aMachi[aMentsu[j][4] - 3] = 1;
						}
						if (aMentsu[j][k] >= 0 && (aMentsu[j][k] % 7) != 0 &&
							(aMentsu[j][k] / 7)*9 + (aMentsu[j][k] % 7) - 1 == aMentsu[j][4]){
							assert(aMentsu[j][4] + 3 < 34);
							aMachi[aMentsu[j][4] + 3] = 1;
						}
					}
				}
			}else{
				/* 面子待ち */
				index = iniMentsu[j] + mindex/3;
				
				if(aMentsu[j][index] < 21){
					/* 順子 */
					pai = (aMentsu[j][index] / 7) * 9 + (aMentsu[j][index] % 7);
					assert(pai >= 0 && pai < 34);
					if((mindex % 3) == 1){
						/* カンチャン */
						aMachi[pai+1] = 1;

						/* 暗刻カブリ */
						if(aMentsu[j][4] == pai + 2){
							if (pai < 0) {
								assert(pai >= 0);
							}
							aMachi[pai] = 1;
						}else if(aMentsu[j][4] == pai - 1){
							assert(pai + 2 < 34);
							aMachi[pai + 2] = 1;
						}

					}else if((mindex % 3) == 0 && (pai % 9) == 6){
						/* ペンチャン */
						aMachi[pai] = 1;
						/* 暗刻カブリ */
						if(aMentsu[j][4] == pai + 1){
							assert(pai + 2 < 34);
							aMachi[pai + 2] = 1;
						}else if(aMentsu[j][4] == pai + 2){
							assert(pai + 1 < 34);
							aMachi[pai + 1] = 1;
						}

					}else if((mindex % 3) == 2 && (pai % 9) == 0){
						/* ペンチャン */
						aMachi[pai+2] = 1;
						/* 暗刻カブリ */
						if(aMentsu[j][4] == pai){
							assert(pai + 1 < 34);
							aMachi[pai + 1] = 1;
						}else if(aMentsu[j][4] == pai + 1){
							aMachi[pai] = 1;
						}
					}else{
						/* 両面 */
						if((mindex % 3) == 0){
							assert(pai + 3 < 34);
							aMachi[pai] = 1;
							aMachi[pai+3] = 1;
							/* 暗刻カブリ */
							if(aMentsu[j][4] == pai + 1){
								aMachi[pai+2] = 1;
							}else if(aMentsu[j][4] == pai + 2){
								aMachi[pai+1] = 1;
							}

						}else{
							assert(pai + 2 < 34);
							assert(pai >= 1);
							aMachi[pai + 2] = 1;
							aMachi[pai-1] = 1;
							/* 暗刻カブリ */
							if(aMentsu[j][4] == pai){
								aMachi[pai+1] = 1;
							}else if(aMentsu[j][4] == pai + 1){
								aMachi[pai] = 1;
							}
						}

					}
				}else{
					/* 刻子はシャンポン待ち */
					assert(aMentsu[j][4] >= 0 && aMentsu[j][4] < 34);
					aMachi[aMentsu[j][index] - 21] = 1;
					aMachi[aMentsu[j][4]] = 1;
				}

			}

			furiten = 0;
			for(k=0;k<34;k++){
				if(aMachi[k] && aAnpai[j][k]){
					furiten = 1;
				}
			}

			if(furiten == 0){
				for(k=0;k<34;k++){
					if(aMachi[k]){
						pKikenhai[k] += 1.0;
					}
				}
			}
			break;

		}


		/* 残った牌を山の数としてカウント */
		for(j=0;j<34;j++){
			pNokorihai[j] += aPaikukan[3][j];
		}
failure:
		j=0;

	}

	/* 試行回数で割り算をして平均値とする */
	for(i=0;i<34;i++){
		pNokorihai[i] /= SIMU_SIZE;
		pKikenhai[i] /= SIMU_SIZE;
	}

	for(i=0;i<27+34;i++){
		pMentsuSimo[i] /= SIMU_SIZE;
		pMentsuToimen[i] /= SIMU_SIZE;
		pMentsuKami[i] /= SIMU_SIZE;
	}

	return 1;
}
```

## C++
```C++
	void MJ0::simulate(Players *players, Player *myself, vector<Pai> *doras)
	{
		PaiArray nokorihai(4.0f), pai_kukan;
		auto mentsu_full = [](Player &p) {return p._mentsu.size() + p._naki_mentsu.size() < 4; };

		for (auto &player : *players) {
			player._anpai = PaiArray();
			player._kikenhai = PaiArray();
			for (auto pai : player._tehai) {
				nokorihai[pai.getNum()] -= 1.0f;
			}

			for (auto pai : player._kawahai) {
				if (!pai.isNaki()) nokorihai[pai.getNum()] -= 1.0f;
				player._anpai[pai.getNum()] = 1.0f;
			}

			for (auto m : player._naki_mentsu) {
				m.sub(&nokorihai);
			}

			player._mentsu.clear();
		}

		myself->_kikenhai = PaiArray();

		for (auto dora : *doras) {
			nokorihai[dora.getDorahyouji()] -= 1.0f;
		}

		for (size_t i = 0; i < SIMU_SIZE; i++) {
			for (auto &player : *players) {
				player._pai_kukan = nokorihai;
			}

			myself->_pai_kukan = nokorihai;

			Players::iterator it;
			while ((it = find_if(players->begin(), players->end(), mentsu_full))
				   != players->end()) {
				while ((it = find_if(it, players->end(), mentsu_full))
					   != players->end()) {
					auto all = Mentsu::all();
					const auto &pai_kukan = it->_pai_kukan;
					// メンツをランダムで抽出する
					float sum = accumulate(all.cbegin(), all.cend(), 0.0f, [pai_kukan](float a,const Mentsu &m) { return a + m.weight(pai_kukan); });
					float val = rand() * sum / RAND_MAX;
					float tmp = 0.0f;
					auto mentsu = find_if(all.cbegin(), all.cend(), [val, tmp, pai_kukan](const Mentsu &m) mutable { tmp += m.weight(pai_kukan); return val < tmp; });
					(it->_mentsu).push_back(*mentsu);

					// メンツを構成する牌を牌空間から除去する
					for (auto &p2 : *players)
					{
						mentsu->sub(&p2._pai_kukan);
					}
					mentsu->sub(&myself->_pai_kukan);

					// 牌の危険度を更新する
					mentsu->addKiken(&it->_kikenhai);
					mentsu->addKiken(&myself->_kikenhai);
					it++;
				}
			}

			for (auto &player : *players) {
				auto all = Mentsu::all_atama();
				const auto &pai_kukan = player._pai_kukan;
				// アタマをランダムで抽出する
				float sum = accumulate(all.cbegin(), all.cend(), 0.0f, [pai_kukan](float a, const Mentsu &m) { return a + m.weight(pai_kukan); });
				float val = rand() * sum / RAND_MAX;
				float tmp = 0.0f;
				auto mentsu = find_if(all.cbegin(), all.cend(), [val, tmp, pai_kukan](const Mentsu &m) mutable { tmp += m.weight(pai_kukan); return val <= tmp; });
				(player._mentsu).push_back(*mentsu);

				// アタマを構成する牌を牌空間から除去する
				for (auto &p2 : *players)
				{
					mentsu->sub(&p2._pai_kukan);
				}
				mentsu->sub(&myself->_pai_kukan);

				// 牌の危険度を更新する
				mentsu->addKiken(&player._kikenhai);
				mentsu->addKiken(&myself->_kikenhai);
			}

			pai_kukan += myself->_pai_kukan;
		}

		myself->_pai_kukan = pai_kukan / SIMU_SIZE;
		myself->_kikenhai = myself->_kikenhai / SIMU_SIZE;
	}
```

## rubyおよびmruby
```ruby
class MJ0
  SIMU_SIZE=5000
  
  def self.simulate(players, myself, doras)
    _nokorihai = Array.new(34, 4.0)
    pai_kukan = Array.new(34, 0.0)
    
    players_w_myself = (players + [myself])
    
    players_w_myself.each do |player| 
      player.anpai = Array.new(34, false)
      player.kikenhai = Array.new(34, 0.0)
      player.tehai.each { |hai| _nokorihai[hai.num] -= 1 }
      
      player.kawahai.each do |hai|
        _nokorihai[hai.num] -= 1 unless hai.naki
        player.anpai[hai.num] = true
      end
      player.naki_mentsu.each { |m| m.sub(_nokorihai) }
      player.mentsu = []
    end
    
    myself.kikenhai = Array.new(34, 0.0)
    
    doras.each { |dora| _nokorihai[dora.getDoraHyouji] -= 1 }
    
    SIMU_SIZE.times do
      players_w_myself.each do |player|
        player.pai_kukan = _nokorihai.clone
        # TODO 手読みで牌空間を変える
      end
      
      while !(subjects = players.select { |player| player.naki_mentsu.size + player.mentsu.size < 4 }).empty?
        subjects.each do |player|
          # メンツをランダムで抽出する
          mentsu = Mentsu.all.w_sample { |m| m.weight(player.pai_kukan) }
          player.mentsu.push(mentsu)
          # メンツを構成する牌を牌空間から除去する
          players_w_myself.each { |p2| mentsu.sub(p2.pai_kukan) }
          # 危険度を更新する
          mentsu.kiken(myself.kikenhai)
        end
      end
      
      players.each do |player|
        # 頭をランダムで抽出する
        mentsu = Mentsu.all_atama.w_sample { |m| m.weight(player.pai_kukan) }
        player.mentsu.push(mentsu)
        # メンツを構成する牌を牌空間から除去する
        players_w_myself.each { |p2| mentsu.sub(p2.pai_kukan) }
        # 危険度を更新する
        mentsu.kiken(myself.kikenhai)
      end
    
      pai_kukan.each_with_index { |pai, idx| pai_kukan[idx] += myself.pai_kukan[idx] }
    end
    myself.pai_kukan = pai_kukan.map { |pai| pai / SIMU_SIZE }
    myself.kikenhai.map! { |pai| pai / SIMU_SIZE }
  end
end
```

# 結果
mrubyはmbrcでバイトコードにコンパイルした後、mruby -bで測定

| 言語 | 測定結果(ms) |
|:---|:---|
| C言語 | 16 |
| C++ | 7 |
| ruby(2.3.1) | 895 |
| mruby(1.2.0) | 4659 |

実行した瞬間目を疑いました、1回の実行で4秒!?なんだってー。C++からrubyでも100倍遅いのでうーんとおもうのですが、mrubyではなんと600倍以上の時間がかかっています。。rubyのコードをもう少し書き直せば多少は速くできるかもしれませんが、ちょっと自分の求める品質(100ms以内)までいける気がせず、ちょっと断念した次第です。

現状mrubyでは軽いスクリプトを走らせることはできるかもしれませんが、AIをがっつり記述して実行するには10年くらい早すぎたようです。後々の改良を待ちましょう。
誰かmrubyのコードを10倍くらい速くする方法をご存じでしたらコメントくださいm(_ _)m