首先说下一个星期
的成果:
- 数据标注了90%。字节数计算比例。好多字节不知道含义多是因为不知道哪里触发这些数据。我虽然找到了他们,但是不知道怎么去用游戏去验证。
- 地雷战主程序函数标注了75%。还有很多懒的标了,且标且看吧。
所以下一步进入了地雷战算法复刻时刻。首先是解析地雷战的路径计算。这是一个上古世纪的游戏,所以它的路径计算方法有点意想不到。不过在花了将近两个小时的情况下,终于明白了它要表达的意思了。下面让我来大家讲解讲解(这篇文章写了四个小时)。
背景
首先,要知道地雷战地图是个塔图。横向X坐标,从左到右。纵向Y坐标,从上到下。而且这个游戏人物移动距离都是很短的。例如大刀队移动距离只有4。
原理
简化
我们把地雷站的路径计算简化为 :
在塔图上,从坐标 (0, 0) 移动到坐标 (dx,dy)。其最佳行走路径是?
假设
我们先有以下假设:
- 假设移动距离为 ,易得知 我们取总数 。小于总数 的每个值都是一种移动可能性 。
- 假设朝向x方向移动的标志位值为 ,朝向y方向的标志为 。
- 此时我们取 的 个标志位。标志位为0,则表明朝x方向移动;标志位为1,则表明朝y方向移动。
- 路径计算的结果路径为 ,总数 的路径为 ,即一个 的列表
计算步骤
计算步骤如下:
- 变量在区间
[0,T)
里循环- 计算的标志位为
1
个数。检查其值和 是否相等。不等,继续下一个。 - 从低往高开始遍历 的标志位 。
- 如果 为0,则是沿x轴移动,如果 大于0,把
R
压入路径列表 。否则,把L
压入路径列表 - 如果 为1,则是沿y轴移动,如果 大于0,把
U
压入路径列表 。否则,把D
压入路径列表
- 如果 为0,则是沿x轴移动,如果 大于0,把
- 计算路径 的代价
- 计算的标志位为
- 循环之后得到 ,即所有可能性 下的路径 的集合
- 对集合 求出代价最小的
原始版本
已经进行了大量的标注,其主函数calc_path
入口地址是4198E5。其功能相当于计算步骤的最外侧
路径计算
void *__cdecl calc_path(int fighter_index, int from_x, int from_y, int to_x, int to_y)
{
void *result; // eax
int v6; // esi
unsigned int path_index; // eax
unsigned int i; // [esp+Ch] [ebp-Ch]
unsigned int j; // [esp+Ch] [ebp-Ch]
unsigned int distance; // [esp+10h] [ebp-8h]
unsigned int paths_len; // [esp+14h] [ebp-4h]
paths_len = 1;
global_path[0] = 0;
result = (void *)from_x;
if ( from_x != to_x || from_y != to_y )
{
v6 = abs(to_x - from_x);
distance = abs(to_y - from_y) + v6;
for ( i = 0; i < distance; ++i )
paths_len *= 2;
paths_list = flc_x90;
memset(flc_x90, 0, 0x1800u);
for ( j = 0; j < paths_len; ++j )
{
check_path_item_at(j, to_x - from_x, to_y - from_y);
calc_path_item_cost(j, fighter_index, from_x, from_y);
}
path_index = calc_best_paths(paths_len);
return memcpy(global_path, (char *)paths_list + 24 * path_index + 4, 0x10u);
}
return result;
}
check_path_item_at
是对可能性path_index
的路径检查,其地址是0x4195e1。
int __cdecl check_path_item_at(int path_index, int deltax, int deltay)
{
int delta; // eax
int v4; // esi
int v5; // esi
unsigned int v6; // [esp+4h] [ebp-10h]
unsigned int i; // [esp+8h] [ebp-Ch]
unsigned int j; // [esp+8h] [ebp-Ch]
int bit_count; // [esp+10h] [ebp-4h]
bit_count = 0;
for ( i = 0; i < 0x10; ++i )
{
if ( (dword_445000[i] & path_index) != 0 )
++bit_count;
}
delta = abs(deltay);
if ( bit_count == delta )
{
*((_DWORD *)paths_list + 6 * path_index) = 1;
v4 = abs(deltax);
delta = abs(deltay);
if ( delta + v4 <= 8 )
{
v5 = abs(deltax);
delta = abs(deltay);
v6 = delta + v5;
}
else
{
v6 = 8;
}
for ( j = 0; j < v6; ++j )
{
if ( (dword_445000[j] & path_index) != 0 )
{
if ( deltay <= 0 )
*((_BYTE *)paths_list + 24 * path_index + j + 4) = 85;// U
else
*((_BYTE *)paths_list + 24 * path_index + j + 4) = 68;// D
}
else if ( deltax <= 0 )
{
*((_BYTE *)paths_list + 24 * path_index + j + 4) = 76;// L
}
else
{
*((_BYTE *)paths_list + 24 * path_index + j + 4) = 82;// R
}
delta = j + 1;
}
}
return delta;
}
路径代价
如果你对它的路径代价计算也有兴趣,我也贴在这里。函数calc_path_item_cost
是计算 的代价。其函数地址是0x419742。
int __cdecl calc_path_item_cost(int a1, int fight_index_, int posx, int posy)
{
int result; // eax
int v5; // [esp+0h] [ebp-14h]
char *i; // [esp+Ch] [ebp-8h]
char v9; // [esp+10h] [ebp-4h]
v5 = 0;
for ( i = (char *)paths_list + 24 * a1 + 4; ; ++i )
{
v9 = *i;
if ( *i == 85 ) // U
{
v5 += calc_path_node_cost(fight_index_, posx, --posy);
continue;
}
if ( v9 == 68 ) // D
{
v5 += calc_path_node_cost(fight_index_, posx, ++posy);
continue;
}
if ( v9 == 76 ) // L
{
v5 += calc_path_node_cost(fight_index_, --posx, posy);
continue;
}
if ( v9 != 82 ) // R
break;
v5 += calc_path_node_cost(fight_index_, ++posx, posy);
}
result = 24 * a1;
*((_DWORD *)paths_list + 6 * a1 + 5) = v5;
return result;
}
路径上每个节点(即步骤描述中的 ) 的代价函数是calc_path_node_cost
。其地址是0x4194C0
int __cdecl calc_path_node_cost(int fight_index_, int posx, int posy)
{
int cost; // [esp+4h] [ebp-14h]
int v5; // [esp+Ch] [ebp-Ch]
int map_z; // [esp+10h] [ebp-8h]
unsigned int z_fighter; // [esp+14h] [ebp-4h]
v5 = 0;
map_z = posx + map_width * posy;
if ( LOBYTE(save_dilei_meishe[map_z]) && HIBYTE(save_dilei_meishe[map_z]) < 0xAu )
v5 = 1;
z_fighter = (unsigned __int8)map_z_people[2 * map_z];
cost = (unsigned __int8)game_w2[16 * *(&fight_people.bingzhong + 33 * fight_index_)
+ ((int)(unsigned __int16)map_data[posx + map_width * posy] >> 12)];
if ( !*(&fight_people.enemy + 33 * fight_index_) && v5 )
cost += 50;
if ( map_z_people[2 * map_z] )
{
if ( *(&fight_people.enemy + 33 * fight_index_) || z_fighter != 1 )
{
if ( *(&fight_people.enemy + 33 * fight_index_) && z_fighter > 1 )// 本人和占格人员都是敌人
cost += 10;
else
cost += 20;
}
else // 友军,green
{
cost += 10;
}
}
return cost;
}
复刻算法
复刻算法采用的Python。你也可以查看这个文件。
'''地雷战算法复原算法'''
def bit_count(v:int) -> int:
return sum([ (v & 1 << i) != 0 for i in range(16)])
class Routing():
'''该算法的地址是4198E5,复原算法对其进行了简化'''
@staticmethod
def calc_dlz(from_x, from_y, to_x, to_y):
return Routing.calc(to_x - from_x, to_y - from_y)
@staticmethod
def calc(dx, dy):
l = abs(dx) + abs(dy)
path_list = []
t:int = pow(2, l)
for p in range(t):
if bit_count(p) != abs(dy):
path_list.append((False))
else:
d = ''
cost = 0
for b in range(l):
if (p & (1 << b)) == 0:
d += 'L' if dx > 0 else 'R'
else:
d += 'D' if dy > 0 else 'U'
path_list.append((True, d, Routing.cost(d, l)))
return path_list
@staticmethod
def cost(path_description, path_length):
return path_length
if __name__ == "__main__":
pl = Routing.calc_dlz(10, 21, 8, 19)
[print(p) for p in pl]
输出对比
复刻算法的输出为
地雷战在输入数据相同时,它的算法输出是(内存数据截图):
怎么样,是不是很完美?!