抗日地雷战算法:(1)路径计算

678 阅读3分钟

首先说下一个星期的成果:

  1. 数据标注了90%。字节数计算比例。好多字节不知道含义多是因为不知道哪里触发这些数据。我虽然找到了他们,但是不知道怎么去用游戏去验证。
  2. 地雷战主程序函数标注了75%。还有很多懒的标了,且标且看吧。

所以下一步进入了地雷战算法复刻时刻。首先是解析地雷战的路径计算。这是一个上古世纪的游戏,所以它的路径计算方法有点意想不到。不过在花了将近两个小时的情况下,终于明白了它要表达的意思了。下面让我来大家讲解讲解(这篇文章写了四个小时)。

背景

首先,要知道地雷战地图是个塔图。横向X坐标,从左到右。纵向Y坐标,从上到下。而且这个游戏人物移动距离都是很短的。例如大刀队移动距离只有4。

原理

简化

我们把地雷站的路径计算简化为 :

在塔图上,从坐标 (0, 0) 移动到坐标 (dx,dy)。其最佳行走路径是?  

假设

我们先有以下假设:

  1. 假设移动距离为 LL,易得知 L=dx+dyL=d_x+d_y 我们取总数 T=2LT=2^L。小于总数 TT 的每个值都是一种移动可能性 pp
  2. 假设朝向x方向移动的标志位值为 00,朝向y方向的标志为 11
  3. 此时我们取 ppLL 个标志位。标志位为0,则表明朝x方向移动;标志位为1,则表明朝y方向移动。
  4. 路径计算的结果路径为 PP,总数 TT 的路径为 RR,即一个 PP 的列表

计算步骤

计算步骤如下:

  1. 变量pp在区间[0,T)里循环
    1. 计算pp的标志位为1个数。检查其值和 dxd_x 是否相等。不等,继续下一个。
    2. 从低往高开始遍历 pp 的标志位 bb
      1. 如果 bb 为0,则是沿x轴移动,如果 dxd_x大于0,把 R 压入路径列表 PP。否则,把L压入路径列表 PP
      2. 如果 bb 为1,则是沿y轴移动,如果 dyd_y大于0,把 U 压入路径列表 PP。否则,把D压入路径列表 PP
    3. 计算路径 PP 的代价
  2. 循环之后得到 RR,即所有可能性 pp 下的路径 PP 的集合
  3. 对集合 RR 求出代价最小的 PP

原始版本

已经进行了大量的标注,其主函数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是计算 PP 的代价。其函数地址是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;
}

路径上每个节点(即步骤描述中的 bb ) 的代价函数是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]

输出对比

复刻算法的输出为

image.png

地雷战在输入数据相同时,它的算法输出是(内存数据截图):

path.png

怎么样,是不是很完美?!

image.png