//天凤牌山生成代码 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 步。
虽然说第三步里面有随机要素,但其实四个人进到一桌坐下来,本质上这一局的牌山都已经安排好了。