玩物丧志(天凤麻雀洗牌代码)

//天凤牌山生成代码 http://tenhou.net/stat/rand/
// http://blog.tenhou.net/article/30503297.html
// 用/* */ 包括的是角田原版注释,以//打头的都是畅畅注释

void SampleYamaShuffle(){
    static const char *haiDisp[34]={
        "一","二","三","四","五","六","七","八","九",
        "①","②","③","④","⑤","⑥","⑦","⑧","⑨",
        "1","2","3","4","5","6","7","8","9",
        "東","南","西","北","白","發","中"
    };

    WORD wShuffleVersionMajor=1;
    int i;
    _MTRAND mtRoot; 

    TCHAR szSeedSeqName[32];//szSeedSeqName:一个宽字符数组 内容是当前系统时间
    {
        DWORD seed[MTRAND_N]; /* これは公開されない*/
        // seed 具体的生成方式是不公开的。这里角田应该考虑到了如果公布了最初种子的生成方式,就可能会有人通过生成真实种子而获得牌山数据,真 “看破牌山”

        if (1){
            // 实际服务器操作 省略了
            HCRYPTPROV hCP; /* for Win32 */

            if (!CryptAcquireContext(&hCP,NULL,NULL,PROV_RSA_FULL,0)) throw 0;
            if (!CryptGenRandom(hCP,sizeof(seed),(BYTE*)seed)) throw 0;
            if (!CryptReleaseContext(hCP,0)) throw 0; //三行异常处理

            SYSTEMTIME st; //查询系统时间
            GetLocalTime(&st);

            wsprintf(szSeedSeqName,_T("%d.%04d.%02d%02d.%02d%02d"),
                wShuffleVersionMajor,
                st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute); //写入 szSeedSeqName

            printf("seedDailyPublic=%s",szSeedSeqName);

            if (1) for(i=0;i<sizeof(seed)/sizeof(*seed);++i) printf(",%08X",seed[i]);
            printf("\r\n");
        }else{
             /* 検証用の入力  szSeedSeqNameをキーに検索 */
             // 输入验证    // 以szSeedSeqName作为关键字搜索
            for(i=0;i<sizeof(seed)/sizeof(*seed);++i) seed[0]=/* REPLACE HERE */0;
        }
        mtRoot.init_by_array(seed,sizeof(seed)/sizeof(*seed)); //mtRoot 用于作为随机数种子,进行 mt199327ar 算法的种子初始化,只进行一次初始化
        //综上所述: mtRoot 是一个可能以系统时间、登录名等东西进行填充,用于随机数生成算法的种子。
    }

    int nGame=0;

    for(;nGame<10;++nGame)
    { 
        /* 配牌が10回行われた場合*/   // 十场比赛 (这里角田是随便举例)

        // 循环使用 mtRoot 这个种子生成随机数
        // mt199327ar 算法:马特赛特旋转演算法产生一个伪随机数,一般为MtRand()。参考链接:http://blog.csdn.net/caimouse/article/details/55668071

        mtRoot.genrand_int32(); /* 席順決定などで3つ消費*/ // 一场比赛用三个随机数确定座位(四个座位只需三个随机数) 
        mtRoot.genrand_int32();
        mtRoot.genrand_int32(); /* 三麻不使用这个 */
        mtRoot.genrand_int32(); /* 未使用 */

        _MTRAND mtLocal; 
        {
            DWORD seed[MTRAND_N]; /* 136!より十分に大きく*/   //它比136大得多!
            for(i=0;i<sizeof(seed)/sizeof(*seed);++i) seed[i]=mtRoot.genrand_int32(); //这里仍然用 mtRoot 不断生成随机数

            mtLocal.init_by_array(seed,sizeof(seed)/sizeof(*seed));// 这些随机数填充 mtLocal,于是 mtLocal 相当于一个新种子

            printf("mt.seed=%s,%d",szSeedSeqName,nGame);
            if (1) for(i=0;i<sizeof(seed)/sizeof(*seed);++i) printf(",%08X",seed[i]);

            printf("\r\n");
        }
        
        /* ここで牌譜にszSeedSeqName,nGame,seedなどを出力 */
        // 在这里输出szSeedSeqName,nGame,种子 等等

        int nKyoku=0;

        for(;nKyoku<10;++nKyoku){ // 配牌が10回行われた場合 (随便举了一个进行十次的牌局)


            DWORD rnd[SHA512_DIGEST_SIZE/sizeof(DWORD)*9]; // 把输出流序列化为无符号4字节整数数组,该数组称为RND
            // rnd 数组,这个数组用于生成牌山。
            // SHA512_DIGEST_SIZE:512/8 , sizeof(DWORD):4 , 所以这个数组的长度是 512/8/4*9 = 144 ,确保了136张麻将牌的顺序、两个色子都放得下。

            {
                DWORD src[sizeof(rnd)/sizeof(*rnd)*2]; 
                // src 数组,这个数组用于用于 SHA 512 散列算法。
                // src 数组的长度是 rnd 的两倍,288

                for(i=0;i<sizeof(src)/sizeof(*src);++i) src[i] = mtLocal.genrand_int32();// 一共循环使用288次 mt199327ar 算法,不断生成随机数,写进二进制流 src 数组。
                

                // 对SRC循环进行 SHA512 哈希。关于 SHA512 参考:https://baike.baidu.com/item/sha-512/3357968

                /* 1024bit単位で512bitへhash*/   // 以1024位散列为单位的512位

                // 简单来说,src 数组是一个输入,经过 SHA512 哈希算法,会产生一个输出。
                // 哈希算法有两条特性:1. 输入稍微有变动输出就有极大变动。 2. 很难只通过输出来构造输入。
                // 哈希算法的特性让角田无法操控牌山。

                for(i=0;i<sizeof(rnd)/SHA512_DIGEST_SIZE;++i){ // 循环次数是:144/(512/8) = 2.25 ,也就是分两次进行哈希,两次哈希分段使用完 src 的输入,两次哈希的输出拼接成一个总的输出。
                    SHA2::sha512_ctx ctx;
                    SHA2::sha512_init(&ctx);
                    SHA2::sha512_update(&ctx,(BYTE*)src+i*SHA512_DIGEST_SIZE*2,SHA512_DIGEST_SIZE*2); // 一次输入 in = 1024 bit。 src 是长度为288的 DWORD 数组,总二进制长度为 1152,也就是分两次输入
                    SHA2::sha512_final (&ctx,(BYTE*)rnd+i*SHA512_DIGEST_SIZE); // 一次哈希输出 512 bit ,两次输出拼接成一个总输出。
                    //输出是放在 rnd 数组里的。
                }
            }

            //最后一步:利用RND数组生成牌山,牌山为长度136的数组,基本思路是对RND数组的元素进行求余
            BYTE yama[136]; // サンマは108  
                            // yama是牌山数组

            for(i=0;i<136;++i) yama[i]=i; // 一开始牌山是做牌做好的,按顺序。
            for(i=0;i<136-1;++i) swap(yama[i],yama[i + (rnd[i]%(136-i))]);  // 然后根据刚才的 RND 数组(里面都是随机数)来 shuffle,也就是打乱牌山。(不断交换两张牌)

            printf("nGame=%d nKyoku=%d yama=",nGame,nKyoku);
            for(i=0;i<136;++i) printf("%s",haiDisp[yama[i]/4]); // 输出牌山
            printf("\r\n");

            int dice0=rnd[135]%6;
            int dice1=rnd[136]%6; // rnd 数组的135 136 用于投骰子

            // rnd[137]~rnd[143]は未使用
        }
    }
}

畅畅总结:

一局天凤麻将游戏中,生成牌山的要素:

1.mtRoot:是一个可能以系统时间、登录名等东西进行填充,用于随机数生成算法的种子。此外,mtRoot还用来决定东南西北的座位

  • mtRoot 的具体生成方式角田并没有公开,如果公开了可能可以伪造种子来知晓牌山。

2.mtLocal:用 mtRoot 生成的随机数,作为新的种子。

3.src 数组,这个数组用于用于 SHA 512 散列算法。内容是以 mtLocal 做种子,288 次随机数算法取的一长串二进制流。

4.rnd 数组:长度为 144。会对 src 进行 SHA 512 散列算法,RND 是存放算法的结果的数组。之后要利用 RND 数组生成牌山。此外,每一局投的骰子也是 rnd 里面来的。

  • rnd 数组的生成结果由于散列算法的性质,保证了公平性。

5.yama 数组,长度 136 。就是牌山本体。思路很简单,一开始里面的数据初始化是 123456 这样的等差数列,然后对RND数组的元素进行求余后不断 swap 就可以打乱(shuffle)。

获取东一局的游戏牌山需要从第1步执行到第5步,第二局(无论是东一一本场还是东二)及之后只需要执行第 3 步到第 5 步。

虽然说第三步里面有随机要素,但其实四个人进到一桌坐下来,本质上这一局的牌山都已经安排好了。

posted @ 2017-12-31 13:19  畅畅1  阅读(1628)  评论(1编辑  收藏  举报