摘要
SE(状态估计)负责把一局中一次次截图读取的观察累积起来,得出“到某个时刻为止”我们知道什么:某个字段当前读到的值、冲突、未知,以及玩家手动输入和撤销。原有的公开接口一次最多处理 64 次读取(read),第 65 次请求会整体拒绝。一局真实对战很容易超过这个数。
本实验实现了一个私有的纯函数(下称“历史累积函数”):输入是整份按时间记录的观察日志(journal)和一个截止时刻,输出是截止时刻为止的部分观察视图,外加一份“实际处理了哪些记录、哪些属于未来被排除”的账本。它不读媒体、不保存会话、不跨调用持有状态。按事先冻结的协议,在 2/64/65 条读取 × 六个截止时刻上运行,21 项全部通过。公共接口、命令行和战斗记录 App 都没有接入。
要回答的问题
- 新函数能否在超过 64 次读取时正确累积,并保留未知、冲突、撤销和未来记录,而不是截断或丢弃?
- 在 64 次以内,结果是否与旧接口完全一致?
- 超出容量时能否整体拒绝、不返回截断的“成功”结果?成本是多少?
数据与来源
- 全部输入是软件构造的观察日志,复用已有的测试数据(fixture),不是新的真实游戏来源;没有读图片、视频或做 OCR。
- 每份日志包含:第 1 次读取有值、后续读数里“法强”为未知;两个用户手动输入的攻击值(0 和 2,互相冲突);同一张图上两次手动设置防御(77 和 78)以及随后两次撤销。
- 64 和 65 条的日志里,倒数第六到倒数第二次读取换成另外五类原生读数(战场、生物面板、法术书、攻击预览、日志),使六类读数都实际经过原校验;书页的归属是明确的软件假设。最后一条是同一张准备页的“未知”重读。
方法
- 先冻结:把完整输入、独立写下的预期结果和容量反例保存到新目录;确认存在后才运行。运行阶段不重新生成输入,也不根据实际结果改预期;第一个意外结果就停止。(首次冻结后、执行前核对上下文规则,调整了五类读数的位置,另存为最终冻结目录,首次目录保留未执行。)
- 函数做什么:先验证全部输入的原生格式、身份、引用和生命周期,再按每条记录的接受时间选出“截止时刻之前、因果闭合”的前缀,用既有的同一个累积逻辑处理整个前缀。未来记录完整保留在请求和“已排除”账本中,但不进入早期视图。
- 账本完整性:每条记录在被实际处理时登记;只有整个前缀投影成功、且访问账本无遗漏无重复,才返回“完整”。
- 严格读取器:另写一个私有读取器,独立计算应有的前缀集合和顺序,检查外层结构、账本、嵌套字段和预算,再重算视图核对。它与函数共享同一个累积逻辑,所以是严格一致性检查,不是独立算法;独立预期值和旧接口对照补充这一限制。
设置
每个向量串行调用一次,在 Python 内存追踪(tracemalloc)开启下计时;不报告 p95、进程常驻内存或冷/热对比。
结果
21/21 通过:18 个语义向量(3 种长度 × 6 个截止时刻)全部符合预期并通过严格读取器;其中 13 个 ≤64 条的前缀与旧公开接口的完整视图相等(除版本名和耗时),5 个完整 65 条前缀被旧接口按预期拒绝;3 个容量反例都整体拒绝。所有请求在调用后保持不变。
65 条日志在六个截止时刻的结果:
| 截止时刻 | 防御值 | 法强 | 实际处理记录 / 未来记录 |
|---|---|---|---|
| 12:00 | 14 | 27 | 1 / 71 |
| 12:01 | 14 | 未知 | 68 / 4 |
| 12:02 | 77 | 未知 | 69 / 3 |
| 12:03 | 冲突 | 未知 | 70 / 2 |
| 12:04 | 77 | 未知 | 71 / 1 |
| 12:05 | 14 | 未知 | 72 / 0 |
后五个时刻,用户攻击值 0/2 保持“冲突”,不补值。冲突时刻准备页组的选定项为空、保留 60 张备选;撤销后选回原读数,59 张备选仍保留。
容量反例:
| 反例 | 请求编码字节 | 结果 |
|---|---|---|
| 65 个同时含准备页和书页的路由读数 | 817,708 | 超出“归一化字段”容量,整体拒绝 |
| 257 个同字段用户手动根 | 85,572 | 超出“证据”容量,整体拒绝 |
| 65 个合法但填充过的单条原始读数 | 17,623,876 | 超出请求字节上限,整体拒绝 |
成本(最后一个截止时刻的单次调用):
| 读取数 | 输入 / 输出编码字节 | 函数秒 / 追踪峰值字节 | 读取器秒 / 追踪峰值字节 |
|---|---|---|---|
| 2 | 4,070 / 53,060 | 0.01919 / 464,168 | 0.02988 / 484,344 |
| 64 | 103,642 / 1,540,106 | 0.58282 / 14,297,254 | 0.99322 / 14,436,242 |
| 65 | 104,737 / 1,566,409 | 0.59775 / 14,546,814 | 1.00790 / 14,639,530 |
失败与负结果
- 首次私有测试 30 通过、1 失败:错误的测试数据把第 2 条读数改成“不支持”却仍让手动设置引用它,原规则正确地拒绝了这个不存在的页面。改为追加一条无引用的独立拒识读数,失败记录保留。正式 21 项实验没有失败。
- 一个注入反例(让登记回调漏掉一条撤销记录)被正确拒绝,说明不能靠简单回显输入 ID 掩盖遗漏。
局限
- 输入是软件构造的旧数据,不是新的真实来源,也不证明游戏规则或识别精度。
- 65 条成功不证明 128/256 条有值页面或整局可用;字节上限的拒绝过程中仍可能有完整 JSON 编码的临时副本,不能把 16 MiB 报文上限当作 16 MiB 内存上限。
- 新私有回归 39 项和受影响旧回归 531 项通过(合计 570 项软件检查),没有跑完整 SE 测试。
- App 侧的新保存版本、修订数和缓存上限、关键帧按需读取、恢复与界面代次等都待联合决定,本批没有修改 App 或其保存格式。
结论与下一步
纯函数路线在有界范围内可行:它能跨过 64 条的限制,同时完整保留未知、冲突、撤销与未来记录,并在超出容量时整体拒绝。下一步是评审一份公共接口的精确提案(唯一入口、版本、字段域、嵌套预算、新旧兼容矩阵),在获得批准和验收之前不导出、不接入 App。
Comments