深入小陆行鸟的世界----主要随机事件分析

2004-08-19 00:00 | bsp

    这篇东东其实是分析chocobo world的存档文件时的副产品,仔细看看其中有些东西还是很有用di,于是组织了一篇帖子出来,希望对大家(尤其是专题,呵呵)能有所帮助。

    对于游戏中出现的随机事件,大数量的重复统计试验不失为一个好的方法,但是效率不高,有些情况下还很难实现(比如对环境要求高的场合,而特定环境的再现较为困难)。这时候对程序的逆向工程就会显示出它的作用,毕竟说话最有分量的还是源代码。

分析chocobo.exe所用工具:Ollydbg 1.09, 很好用的用户级debugger,尤其是支持WinXP和可以给代码作注释,还可以边听MP3边调试。

声明:
    由于众所周知的原因,偶将反汇编出来的代码还原成了伪C代码的格式,并给予相应的注释。相关的反汇编代码,如因学习等原因需要,可以给偶留言索取。对C苦手或者对代码部分根本不感兴趣的朋友,请跳过去直接看相关的分析部分,程序部分在这里只是起到印证分析结果的作用。

1.chocobo world中的随机数产生机理。

    这个可是大前提,当然要先说清楚了才行。FF8的随机数的产生应该与chocobo world的产生方法类似,而且理论上讲这里提到的算法适用于一般游戏中的随机数产生程序。

    产生“随机数”的方法是:通过一个算法由一个初始值(seed)产生一个序列,再通过每次初始化时对seed的改变,达到“随机”的效果。因此,严格说来,计算机产生的随机数应该叫做伪随机数,伪随机数通过算法产生,具有确定性,既可以预测又可以再现。只要算法固定,seed不变,则每次产生的序列都是完全一样的。比较常见的算法是1951年由D. Lehmer提出的线形同余式:Xi+1 = (Xi * a + b) mod m .

代码:
long lRandomX;			//全局变量,待生成的随机变量
void InitSeed(); //通过计算系统时间为lRandomX置初值,只在程序初始化时执行一次
int RandomGenerator(){ //随机数产生函数
lRandomX = (long)(lRandomX * 214013) + 2531011; //注意乘法运算中结果的高32位被截去
return ((lRandomX >> 16) & 0x7FFF) % 256; //取其高16位数据,再模215,再模256,返回
}
分析:

    chocobo world的随机数发生器使用lRandomX的初值作种子,此数值由每次启动chocobo.exe时通过API:GetLocalTime得到的系统时间的 年、月、日、时、分、秒 6项数值参与运算得到,从而确保每次的种子都不同。如果m是2的幂次,产生的数的低位的随机行为不如高位的随机行为,因此程序取其高16位。Xi+1返回时用的是mod 215后的值。因此可以产生0-32767之间的值。
    然后,在具体的程序中只要再mod n(n是一个常数,FF8和chocobo world中经常是256),这个随机数发生器将产生满足随机性要求的0 - n-1之间的数,再和某个固定的数进行比较,即可设定想要的概率。举个例子,程序把通过mod 256后得到的数比较是否小于128,是则执行事件A,那么符合条件的数只能是0 – 127,也就是事件A发生的概率为 128/256 = 1/2。这也解释了为什么游戏中出现的概率都是 xx/256 的形式。
    RandomGenerator()用于程序中的各种随机数的应用,每次产生一个随机数。因此程序中几乎到处可见它的身影。

2.boko的id的种类选择。

代码:
int SelectChocoboType(int iBokoID){
//陆行鸟种类选择函数,iBokoID是ID号的BCD码形式,返回ID号的种类
int iHundred, iTen, iOne; //分别用作ID号的三位数字
SeperateID(iBokoID,&iHundred, &iTen, &iOne); //分离ID号函数
int iIDType=6; //ID号种类,初始为6
if(iOne==7)
iIDType=5; //如果个位为7,那么种类为5
if(iTen==iOne && iOne==7)
iIDType=4; //如果个位和十位都是7,那么种类为4
if(iOne==0 && iTen==0)
iIDType=3; //如果个位和十位都是0,那么种类为3
if(iOne==iTen && iHundred==iTen)
iIDType=2; //如果三位数字相同,那么种类为2
if(iBokoID==0 || iBokoID==777 || iBokoID==008)
iIDType=1; //如果ID号为000、008或者777,那么种类为1
if(iBokoID==211)
iIDType=0; //如果ID号为211,那么种类为1
return iIDType;
}
分析:

    先说一下BCD码,就是用16进制表示10进制,比如BCD码中的16是0x16,等于10进制的22,所以两者的转换需要函数完成。Chocobo world里的HP,Lv,物品数量等都是BCD码的形式存放的。以下代码中的变量,凡以B结尾的均为BCD码形式,不再一一赘述。
    Boko种类的选择在选择其ID时确定,共有7种,分别对应如下:
种类 ID号 个数 
0:2111
1:000,008,7773
2:三位数相同 111,222,333, ... ,9998
3:整百 100,200,300, ... ,9009
4:后两位是77 077,177,277, ... ,9779
5:除以上列出的,末尾是7的ID90
6:剩下的所有ID880


3.Boko得到物品的等级的概率。

代码:
int SelectItemRank(int iIDType){		//获得物品等级函数,返回物品等级
int iRandom = RandomGenerator(); //产生一个0 – 255之间的随机数
int iItemRankPossTable[7][4]= { //物品概率表
{0x40, 0x80, 0xc0, 0xff},
{0x4d, 0x9a, 0xf3, 0xff},
{0x66, 0xdc, 0xf5, 0xff},
{0x7f, 0xde, 0xf8, 0xff},
{0x9a, 0xe1, 0xfb, 0xff},
{0xb3, 0xf1, 0xfd, 0xff},
{0xcd, 0xf3, 0xff, 0xff} };
for(int iBoost=0; iBoost<4; iBoost++)
if( iRandom <= iItemRankPossTable[iRandom % 7][iBoost]) //查表
return 3 - iBoost; //返回对应的物品类型
return 3; //3:D,2:C,1:B,0:A
}
分析:

    物品等级概率在发生仙人掌找到物品的事件时确定,该函数有点隐讳,我详细解释一下:
    首先产生一个概率表,表的行代表 0 – 6,表的列代表对应物品等级的概率,从左到右依次为D, C, B, A:
物品等级各行对应的物品数量
DCBADCBA
00x400x800xc0 0xff65646463
10x4d0x9a0xf30xff78778912
20x660xdc0xf50xff1031182510
30x7f0xde0xf80xff12895267
4 0x9a0xe10xfb0xff15571264
50xb30xf10xfd0xff18062122
60xcd0xf30xff0xff20638120

    然后产生一个随机数,将此数除以7,所得的余数作表的行,然后依次与此行中的数值比较,直到有一个比此随机数大为止,这时所在的列对应的等级即为物品的等级。举例来说,产生的随机数为212 = 0xd4,212 mod 7 = 2,查第三行,0xd4 > 0x66, < 0xdc,对应第二列,于是得到的物品为C级物品。可以看出,物品等级的概率与Boko的种类是无关的,也就是说不论何种陆行鸟,得到的物品等级的概率都是一样的。
    经过计算,得到物品等级与实际概率的关系:
物品等级获得概率
A14/256
B37/256
C75/256
D130/256


    仔细观察那张概率表就会发现,行数正好和Boko的ID种类数量相等。如果行数不是取余数而是Boko的ID种类的话,表的右半部分列出的“各行对应的物品数量”就是各种陆行鸟实际获得物品的概率了(/256),其中211号(种类0)取得各种物品的概率几乎是相等的,而最差的种类6是不可能取得A级物品的。所幸PC版的chocobo world不是这样,不知pocket station上是不是。

4.遇敌的种类。

代码:
#define CREEP	0
#define BAT 1
#define BLOBRA 2
#define GIANT 3
#define BOSS 4 //分别对应了由弱到强的5种敌人
#define BOKO 5 //值得注意的是最后一种…

int SelectEnemyType(int iLevelB){ //敌人种类选择函数,返回敌人的种类
int iLevelH = BCD2HEX(iLevelB);
int nEnemyTypeNumber = 4; //可能出现的敌人的种类数
if(iLevelH < 70)
nEnemyTypeNumber = 3; //70以上最多4种,30-70最多3种
if(iLevelH < 30)
nEnemyTypeNumber = 2; //10-30最多2种
if(iLevelH < 10)
nEnemyTypeNumber = 1; //1-10最多1种
int iRandom1 = RandomGenerator();
int iRandom2 = RandomGenerator();
if(iRandom1 + iRandom2 >=256)
iRandom2 = iRandom1 + iRandom2 - 272;
else
iRandom2 = 239 - iRandom1;
int iEnemyType = MIN( MAX(iRandom2, 0), 255); //相当复杂的随机数计算,详见下文的分析
return iEnemyType * nEnemyTypeNumber / 256;
}
分析:

    敌人种类在进入非boss战斗时确定,首先根据level值(敌人的level和boko的一样)确定可能的敌人种类数量nEnemyTypeNumber。然后产生两个随机数iRandom1和iRandom2,若两数和大于等于256,则取其和-272,若小于,则取239- iRandom1。再取结果与0的较大和与255的较小,确保在0-255之间,最后除以256,得到一个纯小数再与敌人种类的数量nEnemyTypeNumber相乘,确保不会超过它。
    对每个可能取到的数进行计算,就可以得到敌人的出现概率与level之间的关系:
敌人level出现概率
creepbatblobragiant
1 - 1065536/65536000
10 - 3036864/6553628672/6553600
30 - 7026112/6553621760/6553617664/655360
70 - 10020480/6553616384/6553616384/6553612288/65536


    从本段代码开头可以看到,Boko的名字也出现在敌人的列表里。是的,在战斗中把种类改为相应的值后就能看到Boko vs Boko的场面:
Boko vs Boko

我会在修改器chocobo world maker里实现这个功能。

5.HP的计算。

代码:
int iEnemyHPPlusTable[5] = {6, 4, 2, 0, 0};	//敌人HP附加值表
int iEnemyHP = CalculateHP(iLevelB,iEnemyHPPlusTable[iEnemyType]); //计算敌人HP

int CalculateHP(int iLevelB, int iIDType){ //HP计算函数,返回HP值
int iLevelH = BCD2HEX(iLevelB); //转换为一般数字
int iHPPlusTable[7]= {10, 6, 4, 3, 2, 1, 0};//HP附加值表
int iHPBase = 6; //HP基本值
int iHPH = iHPBase + iLevelH/4 + iHPPlusTable[iIDType]; //计算公式
return HEX2BCD(iHPH);
}
分析:

    HP的计算同时适用于Boko的敌人的HP。Boko的HP在挑选陆行鸟和每次升级时确定,敌人的HP在进入战斗之前确定。两者使用的是同一个公式:
当前HP = HP基本值 + 当前level/4(取整数) + 种类附加值

其中,HP基本值固定为6,种类附加值如下表:
种类0123456
附加值10643210

敌人则要多查一次表:
敌人种类01234
对应种类64200

再用这个"对应种类"查一次上面的表,综合起来就是:
敌人种类01234
附加值0241010

    比较两表,可知:敌人的种类比Boko的少,两者的附加值是相反的。后者是由于种类的级别定义不同,Boko的种类号越低越好,而敌人的种类号越高则越强。

6.武器的选择。

代码:
int SelectWeapon(int iHPB){		//武器选择函数,返回武器的4位数值
int iHPH = (iHPB >> 4) * 10 + iHPB & 0x0F;//转换为一般形式
int iRandom = RandomGenerator();
int iWeaponB = 0; //武器
int i; //循环专用超经典变量
int iWeaponNumber; //武器的某一位的值
if(iRandom > 164) //计算方法一,164/256概率
for(i=0; i<4; i++){ //分析中详述
iWeaponNumber = MIN(iHPH, 9);
iWeaponNumber = RandomGenerator() * (iWeaponNumber + 1) / 256;
iWeaponB = iWeaponB << 4 + iWeaponNumber;
iHPH -= iWeaponNumber;
}
else{ //计算方法二,92/256概率
iRandom = RandomGenerator(); //分析中详述
iRandom = (iRandom / 2 + 128) * iHPH / 256;
int iShiftNumber = iRandom % 4;
iRandom /= 4;
for(i=0; i<4; i++){
iWeaponNumber = iRandom;
if(iShiftNumber > 0){
iShiftNumber--;
iWeaponNumber = iRandom + 1;
}
iWeaponNumber = MIN(iWeaponNumber, 9);
iWeaponB = iWeaponB << 4 + iWeaponNumber;
}
}
if(iWeaponB == 0) //如果计算结果全为0
iWeaponB = 1; //那么最后一位是1
return iWeaponB;
}
分析:

    武器选择函数适用于战前的敌人的武器选择和Moomba找到武器事件时的武器选择。武器的选择以HP为依据,注意HP是由??上面的公式计算出来的,boss的实际HP是99,但是按上面的公式计算是41,实际按41选择武器。具体的选择方法则有两种,概率分别为164/256和92/256。下面分述:

    第一种:每次生成武器的一位,方法是从0 - 当前HP(超过9则为0 - 9) 随机选择一个数,再从HP里扣除这次得到的数用于下一位选择。也就是说4位数的和不会超过总的HP,当然HP越高选择较大的数的概率也就大些。举例:HP=15,第一位从0 – 9中选,假设选8,15 – 8 = 7, 第二位从0 – 7中选,假设选4,7 – 4 = 3,第三位从0 – 3中选,假设选0,3 – 0 = 3,第四位从0 – 3中选,假设选2。那么得到的武器就是 8402。

    第二种:首先从 HP/2 – (HP-1) 中随机选择一个数,它除以4的商作武器的每一位的基数,除以4的余数作附加的次数,表示从高位到低位需要附加值的位数,比如3表示前3位都要附加。每一位的数字 = 基数与附加值1(如果该位需要)的和与9的较小的数。可以看出这种算法的有利之处是如果HP很低则武器会有较低的数值,而高HP则高的数值的概率大。举例:HP = 15,从7 – 14中选一个数,假设选11,11 = 2*4+3,那么武器的数值为 4+1 4+1 4+1 4,也就是5554。HP = 68,从34 – 67中选一个数,假设选49,49 = 12*4+1,那么武器的数值为 12+1 12 12 12,超过9的减为9,也就是 9999。

7.boko遇到的事件类型。

代码:
#define ENCOUNTERBOSS	0	//Coco被魔王抓走
#define FOUNDMOG 1 //第一次找到Mog
#define REFOUNDMOG 2 //Mog丢失后再次找回
#define REFOUNDMOGWITHBALL 3 //Mog丢失后再次找回时带来一颗lv up球
#define KISS 4 //Coco kiss Boko,超稀有事件
#define SELECTWEAPON 5 //Moomba找到武器,接下是武器选择和MOOMBASAYBYE
#define FOUNDITEM 6 //Cactuar找到物品
#define ENEMY 7 //普通战斗
#define MOOMBASAYBYE 8 //武器选择完毕后Moomba道别
#define BOSSBATTLE 9 //最终的boss战
#define ENCOUNTERCOCO 10 //第一次遇到Coco
#define SAVECOCO 11 //救起落水的Coco
#define BOKOFALL 12 //Boko落水
#define FIREWORK 13 //一起看焰火

typedef struct{
unsigned int bIsEventWaitOff :1; //事件等待是否为Off
unsigned int bIsEventOccured :1; //特定固定事件是否发生,每次升级都会重设此值
unsigned int bIsBossDefeated :1; //boss是否已被击败
unsigned int bIsMogStand :1; //Mog是否stand
unsigned int bIsMogHere :1; //Mog是否在身边
unsigned int bIsMogEverFound :1; //Mog是否曾经找到过
unsigned int bUnknown :1; //??
unsigned int bUnknown :1; //??
}byGameStatus;

int SelectEvent(int iLevelB, int &byGameStatus, int iMove, int &iChocoStar, bool &bIsFriendVisited){
//事件选择函数,byGameStatus是游戏中常用状态,iMove是Move数,iChocoStar是Boko的星数
//bIsFriendVisited是上个事件是否为Moomba或Cactuar,返回事件的号码
int iLevelH = BCD2HEX(iLevelB);
switch(iLevelH){
case: 20
if(byGameStatus.bIsEventOccured)
break; //此事件已发生
byGameStatus.bIsEventOccured=1;
return ENCOUNTERCOCO; //第一次遇到Coco
case: 75
if(byGameStatus.bIsEventOccured) //此事件已发生
break;
byGameStatus.bIsEventOccured=1;
return FIREWORK; //一起看焰火
case: 50
if(byGameStatus.bIsEventOccured) //此事件已发生
break;
byGameStatus.bIsEventOccured=1;
if(byGameStatus.bIsEventWaitOff == 0){ //事件等待为On
iChocoStar++;
return SAVECOCO; //救起落水的Coco
}
return BOKOFALL; //Boko落水
case: 100
if(!byGameStatus.bIsEventOccured){ //此事件未发生
byGameStatus.bIsEventOccured=1;
return ENCOUNTERBOSS; } //Coco被魔王抓走
if(iMove != 1)
break; //move必须为1
if(byGameStatus.bIsBossDefeated)
break; //魔王不能被打败
return BOSSBATTLE; //最终的boss战
}
int iRandom = RandomGenerator();
int iTemp = iLevelH - 10;
if(iTemp < 0)
iTemp++;
iTemp/=2; //设定概率为((level – 10)/2)/256
if( iRandom < iTemp && byGameStatus.bIsMogEverFound == 0){ //Mog没有找到过
byGameStatus.bIsMogHere = 1;
byGameStatus.bIsMogEverFound = 1;
return FOUNDMOG; //第一次找到Mog
}
iRandom = RandomGenerator();
if(iRandom < 128 && byGameStatus.bIsMogEverFound && !byGameStatus.bIsMogHere){
// 1/2的概率,Mog曾经找到过,Mog不在身边
iRandom = RandomGenerator();
if(iRandom >=32){
byGameStatus.bIsMogHere = 1;
if(iLevelH != 100) //等级不为100
return REFOUNDMOGWITHBALL; //Mog丢失后再次找回时带来一颗lv up球
}
return REFOUNDMOG; //Mog丢失后再次找回
}
iRandom = RandomGenerator();
if(iRandom == 0){ // 1/256 概率事件,呵呵
if(iChocoStar >= 3) //Boko的星数为3
return ENEMY;
if(iLevelH < iChocoStar * 13 + 63) //等级低于 星数 x 13 + 63
return ENEMY;
if(iEventWait == off) //事件等待为Off
return ENEMY;
if(iLevelH == 100 && bIsDeamonDefeated)//100级打败魔王
return ENEMY; //普通战斗
iChocoStar++; //Boko的星数加一
return KISS; //Coco kiss Boko,超稀有事件
}
if(iRandom < 48){ // 47/256概率事件
if(bIsFriendVisited) //朋友来过?
return ENEMY;
bIsFriendVisited = true;
return SELECTWEAPON; //Moomba找到武器
}
if(iRandom < 112){ // 64/256概率事件
if(bIsFriendVisited)
return ENEMY;
bIsFriendVisited = true;
return FOUNDITEM; //Cactuar找到物品
}
bIsFriendVisited = false;
return ENEMY; //其余全为普通战斗
}
分析:

    事件选择在Boko走到事件点时触发,各事件发生条件如下:
固定事件:
事件发生条件
第一次遇到Coco20级,仅一次
一起看焰火75级,仅一次
救起落水的Coco50级,事件等待必须为On,仅一次
Boko落水50级,事件等待必须为Off,仅一次
Coco被魔王抓走100级,仅一次
最终的boss战100级,Coco被魔王抓走,Move值为1,魔王未被击败


随机事件:
事件发生条件
第一次找到MogMog未找到过,12级以上(含12级),概率 ( (level - 10) /2) /256, 12级时 1/256,100级时 45/256
Mog在战斗中丢失后再次找回Mog曾经找到过,Mog丢失,概率 1/2再次找回时带来一颗lv up球低于100级,概率 224/256
再次找回时不带球概率 32/256
Coco kiss BokoBoko的星数低于3(注),事件等待必须为On,等级必须不低于 星数 x 13 + 63,未击败魔王,概率 1/256
Moomba找到武器上一次事件不是武器也不是物品,概率 47/256
Cactuar找到物品上一次事件不是武器也不是物品,概率 64/256
普通战斗以上的事件以外的情况


注:Boko的星数 是指菜单里武器右边显示的星的数目,此数目与FF8里的小陆行鸟的召唤技息息相关,数目越大召唤技的级别越高。
    50级的事件分两个,事件等待为On和Off各一个,后者是不能使Boko的星数增加的。
    这里特别需要提到就是Coco kiss Boko事件,我想这也是大家最关心的。这个事件的发生概率极低,只有1/256,要求的条件也是很苛刻:首先Boko的星数不能为3(废话),事件等待为On(这个意味着不能挂机来等到它),等级方面的对应关系为:0颗星:lv63以上,1颗星:lv76以上,2颗星:lv89以上(很高的等级啊),若已经100级了则千万不要急着打败魔王,否则就再也没有可能出现这一事件了。总之,50级的事件时等待一定要为On,63级以后您就慢慢耗着吧,到了100级也没关系,只要不打败魔王,总有机会出现这一事件di~~~

    呼,终于写完了,好累。感谢阅读,欢迎挑错与874,如果对程序部分的可信度有疑问,可以给偶留言索取反汇编代码,留言中留下您的邮箱,再申明一遍,不要将反汇编代码发布和用于商业目的