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