摘要

在《英雄无敌:上古纪元》里,每一堆生物(兵堆)的数量会随着战斗减少(减员),偶尔也会因复活而回升。状态估计(SE)通过在两次行动之间读行动条卡片、战场数量牌、面板上的数量,以及战斗日志(Logs)里的“被摧毁”行,累积出每一堆的减员时间线。

战斗结束时,游戏会弹出结算画面,列出双方每一堆的开局数量和整场总损失。这正好可以用来核对 SE 的累计结果。外部审阅把“结算画面读取及终局核对”列为优先事项。当时结算画面读取器还在并行开发,所以本实验先用从 960×540 帧上模型目视读出的结算数字(MODEL_VISUAL_CHECKED,不是人工审核 REVIEWED)作为参考。

两局训练录像的结果很不一样:

  • 10 月 10 日 16:57 局:14 堆开局数量全对;净减员一致 8 堆;“缺最后一下”6 堆;不一致 0 堆。
  • 10 月 10 日 01:39 局(旧基线):14 堆开局全对;一致 3 堆;缺尾 11 堆,其中 9 堆是战斗中途整堆消失、行动条上不再出现,却没读到“被摧毁”日志,时间线就停在最后一个读数上;不一致 0 堆。

把结算读数作为最后一帧交给 SE 后,所有缺尾的堆都补上了最终的 0,但对于中途消失的堆,补上的时刻比实际消灭晚很多(最长约 20 分钟)。

要回答的问题

SE 整场累计的减员与结算画面的开局数量和总损失是否一致?不一致时,错在哪里?结算画面能不能补上 SE 的缺口?

数据与来源

  • 两段用户选定的训练录像的 App 回放会话:16:57 局一次端到端回放(260 帧请求);01:39 局一次旧回放(550 帧:41 张关键帧截图 + 509 个行动条 / 预览 / 信息页旁路读数,快照标记为未完成)。01:39 的回放是在当晚“战斗阶段把关”和“按画面时间排序”两个修复之前跑的,所以是旧基线。
  • 结算参考:从结算画面帧上模型目视读出的每堆开局数量和损失。01:39 局:左边红旗是敌方、右边蓝旗是我方(录像方胜,只有右边留下一堆)。
  • 只读,没有写入新的录像目录。

方法

在 App 保存的 SE 请求上重跑 SE(放开每堆最多保留 24 次读数的上限),对结算画面上的每一堆比较:

  • 开局:SE 第一次读数 vs 结算的开局数量;
  • 净减员:SE 的减员合计减去回升合计 vs 结算的损失。

分类:

  • MATCH:净减员相等;
  • MISSING_END:SE 最后一次读数高于结算后的数量(最后的损失发生在最后一次读数之后);
  • MISMATCH:其他情况;
  • NOT_TRACKED:SE 没有读数。

结算画面上没有、SE 却有的堆单独列出。不为了对上而改 SE 的历史。

后来 SE 正式接入了结算读数,又做了一个对照:在原请求末尾加一帧按参考构造的“已稳定结算读数”(兵种全部为空,只能按开局数量对应)。对 01:39 局另外比较了“按保存顺序”和“按画面时间重排”两种帧顺序。

设置

16:57 局用旧版和新版核对脚本各跑一次,打印与输出完全相同。

结果

16:57 局:

结果 堆
开局数量 14 堆全部与 SE 第一次读数相同
MATCH 8 敌方圣殿狮鹫 48、羽蛇 18;我方骷髅 385→336(SE 减 60、回升 11)、深渊使者 12→9、不死宠物 134→130、死亡商人 50→50(减 1、升 1)、修道院长 16→8、圣骑兵 18→6
MISSING_END 6 敌方剑士(最后读到 26)、神化者(1)、织光者(6)、骑兵(8)、妖精巨龙(4);我方巫妖(1)
MISMATCH 0

MISSING_END 的含义:打出结束战斗的那一下之后,不再有行动条或日志可读;整堆被消灭而那段时间没读到“被摧毁”时,时间线只能停在最后一个读数。结算画面恰好补上这个终局缺口。

SE 有、结算没有的堆:我方星之子(21 次读数,多半是召唤物,结算画面不列召唤物);我方鬼魂 50(8 次,待查:召唤物或兵种认错);另有几堆各 1–2 次读数的,多半是行动条或数量牌把兵种认错(同一时刻真正的兵堆是羽蛇 18、死亡商人 50 等)。敌方神化者 SE 减员 100、回升 94、净 6:中间一次把别的数量当成神化者又改了回来,净值没错但过程是噪声。

加构造结算帧后:MATCH 7、MISSING_END 6、UNMATCHED 1、MISMATCH 0;6 个缺尾都在结算帧补上了最后的减员(例如敌方剑士 26→0)。UNMATCHED 是我方死亡商人 50:第一次数量变化之前 SE 读到 3 个开局 50 的堆(其中两个是认错的兵种),按数量对应不唯一。

01:39 局(旧基线): 550 帧全部有时间;保存顺序里有 15 处相邻两帧时间倒退(最多退 4.5 秒),重排后 34 个位置变化。但两种顺序下每堆的结果(开局、最后读数、减员、回升、读数次数)完全一样,所以下表对两者都成立。

结果 堆
开局数量 14 堆全部相同
MATCH 3 我方凤凰 10→0(日志“被摧毁”)、真菌伊利亚德 52→26、晨曦蛇麻虫 60→0(日志“被摧毁”)
MISSING_END 11 敌方修道院长 16→1、观世者 18→18、太阳圣盾 140→140、织光者 60→6、神化者 7→3、圣殿狮鹫 48→3、驯鹰师 110→11;我方判官 26→1、麒麟 24→3、羊人 135→96、水泽精灵 54→18
MISMATCH / NOT_TRACKED 0 / 0

结算画面约在 1797.6 秒出现。与 16:57 不同,这一局的缺尾多数不是“最后一下”:只有修道院长(1795.9 秒)和麒麟(1777.4 秒)的最后读数贴近终局;其余 9 堆是战斗中途整堆消失。最明显的是敌方观世者 18 和太阳圣盾 140:551.8 秒的行动条上还有这两张卡,554.0 秒已经没有,SE 一次减员都没记。按现有规则“卡片不再出现本身不当作 0”,这是设计内的缺口,不是读错。

SE 有、结算没有:一次把晨曦蛇麻虫认成其未升级形态;一次把当时正好是 18 的水泽精灵认成天使 18。我方麒麟减员合计 226、回升合计 205、净 21:中间三次把别的数 79 读成麒麟,净值对,但过程是噪声。

加构造结算帧后:MATCH 2、MISSING_END 11(全部补上最终的 0)、UNMATCHED 1(我方 60:开局变化前有两堆 60,按数量对应不唯一)、MISMATCH 0。

SE 请求规模(把 260 帧请求重复拼接,只测大小,不是更长的对局):

帧数 请求 JSON 第一次 之后三次中位 结果 JSON
260 3.5 MB 27.7 秒(冷启动) 0.52 秒 0.46 MB
520 7.1 MB 1.63 秒 1.20 秒 0.72 MB
780 10.6 MB 2.35 秒 1.77 秒 0.98 MB
1024 14.0 MB 2.84 秒 2.19 秒 1.23 MB

约每帧 2 毫秒、近似线性;上限 1024 帧可用,但很长的对局每次重算会到 2 秒左右,实时模式要盯这一项。暂不压缩历史。

失败与负结果

  • 01:39 局 9 堆中途消失没被记录,是现有规则下的设计缺口。
  • 两局都有兵种认错导致的“多出来的堆”和中间读数噪声(净值仍对)。
  • 内存峰值没有测成:脚本把进程句柄当 32 位整数传,调用失败、显示 0;之后已修(按指针宽度传、失败时报错),待机器空闲时重测。

局限

  • 结算数字是模型目视参考,构造的结算帧兵种全为空;真实读取器的输出可能不同。
  • 01:39 局是旧基线、快照未完成;我方几乎全灭、没有复活,无法核对结算“总损失”是否为净损失(16:57 局的骷髅减 60、回升 11、结算写 −49,支持净损失)。
  • MATCH / MISSING_END 只比较第一次和最后一次读数,中间读错不影响分类,也不计数。
  • 计数不是准确率;每局只有 14 堆。

结论与下一步

结算画面是整场减员的有力核对:开局数量两局全对,没有一堆“对不上”;但它暴露了两种缺口——终局最后一下读不到(16:57 局),以及战斗中途整堆消失没被记录(01:39 局)。结算能补上最终数,但补不回准确的消灭时刻。

下一步:用读取器在真实结算帧上的输出替换目视参考;核实结算“总损失”的语义(复活和召唤怎么算);把认错的兵种交给行动条金标准去量化;把“行动条上整堆消失”列为消灭的候选证据提案(需与日志、召唤、隐身等区分,先不直接改 SE);等 01:39 新回放跑完后重新核对。