开发日志(四):事件回放与批量统计
一、回放与模拟功能的改进
1. 从重跑到事件回放
前三次迭代已经能运行对局并保存事件,但第 2 版日志的“回放”仍须按原种子重跑。第四次提交改为读取已记录事件来重建对局状态,避免策略或随机数调用顺序变化时旧对局随之改变。第 3 版 JSON 日志因此保存完整地图快照、事件顺序、模拟步骤,以及默认开启的角色状态快照。
ReplayTimeline 从 SimulationTrace 读取事件,维护角色位置、生存状态、探索记录和尸体状态;Advance 应用下一条事件,Seek 移动到指定事件位置,VerifyComplete 检查终态。它只应用已记录的结果,不再调用 NPC 策略决定动作。
ReplayActor 只保存回放所需的角色状态,不重新实例化完整战斗实体。这样应用旧事件时不会意外触发新一轮战斗、背包操作或 AI 决策。
日志区分三种顺序信息:Step 是模拟器的推进次数,Turn 是对局回合,Sequence 是连续编号的事件次序;一次推进可能产生多条事件。ActionEvent 还记录行动者、事件类型、起终点、目标、说明及可选的观测或状态快照。地图快照逐房间保存类别、名称、可进入性和四向通道。回放由此直接使用当时的布局,无须按种子重新生成地图。
ActionJournal.Load 先检查版本、地图尺寸、房间坐标和事件编号。ReplayTimeline.Advance 再核对出生、移动、探索与死亡事件所要求的角色存在性、位置和生存状态;状态事件核对位置与生死,并读取血量、伤口、装备和背包快照。最后,VerifyComplete 检查结束事件、步数、结束原因、胜者和存活人数。
2. 两层回放视图:状态与展示分开
ReplayMapCanvas 从 ReplayTimeline 读取地图和角色位置,按画布与地图尺寸计算单格大小,不维护第二份可变状态。ReplayViewer 负责打开日志、播放与暂停、调整速度和进度,并显示事件历史及角色列表。
时间轴决定当前事件对应的状态,画布只负责绘制该状态。因此更换绘制风格无需改变事件应用规则;调整日志读取方式也不应要求画布重新实现对局逻辑。
模拟器 / GameController → ActionJournal → SimulationTrace(JSON)→
ReplayTimeline → ReplayViewer / ReplayMapCanvas3. 决策接口的长期价值
ActionJournal.cs 定义 AgentObservation、AgentAction 和 IAgentPolicy,SimulationRunner.Run(settings, policy) 接受外部策略。现有 NPC 策略可以继续使用同一入口;以后加入规则基线或随机策略,也能在相同对局配置下比较。
策略只接收 AgentObservation,输出候选 AgentAction;控制器再检查并执行动作。这条边界限制策略访问角色本不应知道的世界状态。接口允许替换决策方法,但本次提交并未实现强化学习训练。
4. 批量统计不只看平均值
新增 BatchStatistics,提供 BatchRunSummary、BatchReport 和 MeanStatistic,汇总每局结束情况与角色表现。命令行可以执行单局 run、多局 batch 和日志 replay;批量结果同时写出 summary.json 和 runs.csv,便于后续分析。
各均值使用不同的有效样本:结束回合只统计正常结束的对局,首次遭遇回合只统计出现遭遇的对局,死亡回合只统计该角色死亡的对局。MeanStatistic 同时记录 Count 与可空的 Average,避免把“未发生死亡”填成第零回合。
命令行的三个入口对应三种工作方式:run 运行并保存一局,batch 连续运行多局并生成每局 JSON、summary.json 与 runs.csv,replay 只读取既有日志并验证。run 与 batch 可分别指定地图种子和过程随机种子;批量指定种子时,第 局取基础种子加 。这让“同一地图、改变 AI 随机过程”和“整组实验可复现”成为可操作的选择。达到最大步骤的对局写为 StepLimit,命令返回非零状态,避免把未分胜负的一局误计为正常结束。
CompletedCount 包含所有未达到步数上限的终局,WinsByActor 只统计有胜者的终局,因此两者可能不同。死亡回合按角色 ID 汇总;每局首次遭遇取第一条 Encounter 事件。
二、视觉层响应已结算事件
本次新增 BloodSplashOverlay,根据攻击类型显示枪击或斩击死亡特效,并在特效结束后再放行结局面板;CorpseShadowLayer 也加入血迹扩散状态。效果层响应已发生的死亡事件。
同时 RoomVisual、GameViewPreview 和两个 UI Smoke 检查从 GDScript 迁移为 C#。这减少了同时维护两套脚本语言的成本,也让 Godot 编辑器预览与运行时尽量使用相同的场景节点和绘制代码。
三、回放验证的边界
当前日志保存了完整地图,因此新版整图回放不依赖种子重算;旧版只有种子、没有地图的日志不能使用整图回放。达到最大步骤而没有结束的对局标记为 StepLimit,与正常获胜或同归于尽区分。
后续值得考虑版本升级兼容、日志体积控制、定期检查点与快速跳转性能,以及事件是否足以恢复全部重要游戏状态。当前 Seek 在向前跳转时逐条应用事件,向后跳转会清空回放状态再从头应用,因此长日志上的反复拖动可能变慢。还应把“以新日志回放得到的终态等于原对局终态”做成持续集成中的自动断言,并对背包、血量等字段建立更强的独立校验。
四、战斗数值与事件解释
命中、追击和行动优先权曲线已在第三次提交进入 Combat.cs。本次沿用这些数值,并在战斗结果中记录武器类型,以便显示与实际致死攻击相符的效果。下面集中说明第四次提交时运行的战斗规则;曲线本身并非本次新增。
防护不再直接降低命中率。Character.MaxHealth 取基础最大生命与防护值的较大者,命中后调用 TakeDamage(1)。基础生命为 1、防护为 3 的怪物因此需要承受三次有效伤害才会死亡。分析记录时应区分出手、命中和击杀次数:未命中由命中曲线决定,命中后未击杀则可能由防护带来的承伤能力解释。
代码将追击视为武器提供的被动等级:追击不消耗子弹,也不产生枪声暴露。本次提交进一步把追击结果的武器类型标记为 Melee。若把追击错误地当作一次枪击,除了弹药会被错误扣除,屏幕死亡特效也会显示错。因此 CombatRoundResult 不能只有一个“命中/未命中”布尔值,还必须保留本次有效攻击的来源。
探索触发的遭遇首轮禁止回退,代码会拒绝此时传入回退选择;普通遭遇则允许。若双方同时回退,本轮直接结束。若双方都攻击,才进入先后行动与普通命中判断。这三个分支对应不同的概率和资源消耗。
1. 弹药耗尽后仍要正确记录这一击
CombatResolver.Attack 在消耗弹药前保存本次出手使用的武器及其命中概率。若枪械打出最后一发,Character.TrySpendAmmo 可能立即将装备切换为背包中的近战武器;此后才读取武器类型,就会把已经发生的枪击误记为刀击。本次在 CombatRoundResult 中记录双方出手时的 WeaponRange,使结果与装备切换前的攻击一致。
BloodSplashOverlay 根据记录的武器类型选择枪击或近战特效,播放约 1.25 秒后再显示结局面板。CorpseShadowLayer 在房间中加入新死亡者,并播放约 1.35 秒的血迹扩散。两种效果的持续时间不同,但都读取已经结算的死亡结果,不参与命中或死亡判定。
2. 为什么数值曲线需要进入回放与统计
第 3 版日志中的 CombatRound、Death、State 等事件,分别回答“这一轮发生了什么”“谁在何处死亡”和“结算后各角色是什么状态”。整图回放按事件推进,因而能看到同一房间的遭遇、角色消失和尸体标记;批量统计则能从事件中提取首次遭遇、死亡回合及最终胜者。
要评价曲线平衡,还需要比现有 BatchReport 更细的指标。比如按双方等级差分组统计出手与命中,比较表中的理论概率和模拟观察值;单独统计追击机会与追击击杀;再分出“命中但被防护承受”的次数。当前批量报告聚焦结局、回合与胜者,尚未生成这些战斗分布。将来添加时应保留每组样本量,并区分玩家、普通 NPC 与怪物,否则少数高防护怪物就可能掩盖人类之间的命中表现。
3. 种子的控制
命令行以 --seed 控制地图,以 --random-seed 控制对局过程;只指定地图种子时,程序也用它作为过程种子。批量运行若给定基础种子,每局种子按序递增。固定这些输入可在当前代码版本下重复一组实验,用于比较策略或检查改动后的结果。
事件回放读取的是第 3 版日志中的地图快照及出生、移动、探索、战斗、死亡和状态事件。ReplayTimeline 不抽取新随机数,也不重新调用 NPC 策略。即使将来调整命中曲线,旧日志仍按原记录展示;新规则的实验则需另行运行。这样比较规则版本时,历史对局不会被当前实现重新解释。
4. 批量报告如何解读
假设二十局中十六局正常结束、四局达到步数上限,平均结束回合只使用前十六局;如果只有十二局发生过遭遇,平均首次遭遇回合的样本数就是十二。某角色若只在三局死亡,它的平均死亡回合只来自那三局,而不能把其余十七局当作“第零回合死亡”。MeanStatistic 记录 Count 与可空的 Average,正是为了显露这种分母差异。这里的数字仅是解释口径的假设例子,并非仓库实际批量运行结果。
报告同时保留每局的地图种子、过程种子、结束原因、胜者、事件数及日志文件路径。看到两个策略平均死亡回合不同时,应先比较样本数、正常结束比例和 StepLimit 比例,再回到具体 JSON 查看关键战斗。若某策略只是更常拖到步数上限,它在“只统计已结束对局”的均值里可能显得更好,却未必真的更能生存。数值曲线的平衡分析需要这些原始记录作支撑,不能只用一列平均值下结论。