Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

やろうとしたこと

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

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

今までは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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away