没有多数时不要抓战犯:同步分歧应先把房间标成可疑

同步状态在无法形成严格多数时进入可疑房间与证据升级流程的专业主视觉

两人对战在第 300 帧上报不同的状态哈希。系统知道双方已经分叉,却不知道谁错。若产品要求“检测到不同就立刻踢一个”,工程实现只能借助座位号、先到顺序或网络质量猜测。这样的自动处置看似维持了比赛,实际上把缺失的证据伪装成确定结论。

四人房间也不一定更容易。两人报告 A,两人报告 B,任何一边都没有超过在线人数的一半;四份完全不同的结果更无法归责。此时真正的问题不是如何选出赢家,而是系统要承认自己的观测能力已经不足,并把房间从“可正常裁决”切换到“需要升级处理”的状态。

TankPlay 的仲裁器没有在无多数时处罚任何座位。它累计连续无多数次数,达到三次后把房间标记为 suspect。这个设计的价值不在于一个布尔字段,而在于它把“不知道谁错”建模为正式状态。未知不再被藏在日志里,也不再被粗暴转换成玩家责任。

无多数含义

两人一比一、四人二比二和三人完全分裂时的无严格多数决策矩阵

严格多数要求最高票数满足 count > floor(N/2)。两名在线玩家各持一个哈希时,最高票数为 1,门槛也是 1,但条件是“大于”,所以不能裁决。四人 2:2 时同样失败。三人 1:1:1 也没有任何哈希获得两票。

这和“找到票数最多者”是两种不同问题。票数最多只保证排序第一,严格多数才保证同一轮中不可能存在另一个同样有效的相反结论。仲裁器要据此处罚少数座位,就必须使用后者;否则平票时由哈希表遍历顺序选出的结果,也可能被误当成架构决定。

无多数至少对应三类原因。第一类是真实确定性分叉,每个客户端都沿不同状态继续计算。第二类是版本或资源不一致,部分客户端共享一套结果,另一部分共享另一套结果。第三类是观测集合错误,例如旧连接、重复席位或断线状态没有及时收敛。三类问题的恢复责任分别落在战斗逻辑、版本准入和连接管理上,仲裁器本身无法从哈希计数判断是哪一类。

所以 suspect 的语义必须精确:它表示房间连续无法形成可归责的共识,不表示所有玩家作弊,也不表示某个哈希正确。若监控系统把 suspect=true 直接翻译成“作弊房间”,运营和风控就会在错误语义上继续放大误判。

连续三次

一次无多数可能来自检查点附近的暂态问题,例如成员集合刚变化、旧报告刚被清理,或者某个版本缺陷只影响特定事件。当前实现不在第一次分裂时升级,而是增加 no_majority_streak;形成一次严格多数后,这个连续计数会清零;连续三次无多数后,suspect 变为真。

这是一个带恢复边的状态机,而不是简单累计总次数。序列“无多数、形成多数、无多数、形成多数”永远不会达到三次,因为中间的有效裁决证明房间至少暂时重新收敛。相反,三个连续检查点都无法归责,说明问题已经跨越多个观测窗口,值得进入更昂贵的诊断或终止路径。

仍然要注意,三次不是脱离频率的绝对尺度。若每 15 帧检查一次、逻辑频率为每秒 30 帧,三个检查点覆盖约 1.5 秒;若每 300 帧检查一次,则覆盖约 30 秒。真正的架构参数是“连续次数 × 检查点间隔 ÷ 逻辑帧率”。项目当前把连续次数固定为 3,适合先锁定行为;当不同玩法的检查频率显著分化时,才需要把时间窗口提升为模式级决策。

suspect 一旦置真不会在后续多数形成时自动恢复。当前代码只清零连续无多数次数,没有清除房间可疑标记。这是合理的证据保留:后续重新一致只能说明分歧结束,不能证明此前三个窗口中的异常没有影响比赛结果。清除可疑状态应该由更高层完成,例如重建房间、完成回放核验或明确放弃本局结果,而不是由下一轮投票顺手洗掉历史风险。

在线集合

检查点是否完成,以当前 online_seats 为准。每次合法报告进入后,仲裁器先删除已经不在线座位的旧投票,再用当前在线人数判断是否收齐。这条顺序解决了一个很具体的竞态:座位 2 已经在三人状态下提交,随后断线;座位 0、1 以两人在线集合继续上报。座位 2 的旧票必须先移除,否则它会在已经失去成员资格后继续影响多数。

本地测试覆盖了这条路径:座位 2 先以三人集合提交不同哈希,之后座位 0 以 [0,1] 上报;检查点仍保留,直到座位 1 也提交才完成,而且不会因为离线者的旧票把房间标记为可疑。这里验证的不是一个容器操作,而是“当前成员关系覆盖历史报告”这一不变量。

但该设计也明确依赖调用方:online_seats 必须是当前房间成员的可靠快照。若断线检测过慢,已经消失的座位仍在集合里,检查点会等待它的报告;若重连时旧连接和新连接都被映射成同一座位,迟到报告可能覆盖新报告。仲裁器用座位号作为投票键,无法区分同一座位的连接世代。

因此,连接层至少应保证两个条件:一个座位在任一时刻只有一个有效会话所有者;成员变化与提交仲裁报告之间有明确顺序。需要更强隔离时,报告键应从 seat 演进为 (seat, session_epoch),但只有在旧连接迟到已经成为真实问题时才值得增加这个维度。现阶段,复用房间层已有的连接所有权检查,比在仲裁器里另造一套会话系统更可靠。

有限窗口

在线成员变化时清理离线旧票,并用两个槽位限制未完成检查点

等待所有在线成员会带来活性风险:某个检查点迟迟收不齐时,新检查点仍可能到达。若每个未完成帧都永久保留,攻击者或异常客户端只需持续缺席,就能让房间状态随运行时间增长。

当前实现把待处理检查点硬限制为两个。新帧第一次进入且容器已满时,删除帧号最小的旧检查点,再为新帧建立记录。若依次只收到第 15、30、45 帧的一份报告,最终保留 30 和 45,15 被淘汰。对应测试直接断言旧帧不存在、后两帧仍存在、总数等于上限。

空间上界因此很容易复算:在线人数为 N,每个检查点最多 N 份 seat → hash,最多两个检查点,核心投票存储约为 2N。更重要的是时间上界:仲裁器不会为了等一份永远不到的报告,把最老观察无限带入未来。

代价也必须说清楚。淘汰旧检查点意味着丢失一次可能尚未完成的归责机会;如果第三个检查点到来前,旧帧最后一份报告刚好在路上,它到达时可能重新建立一个已被淘汰的旧检查点,并挤掉当前窗口中的另一个帧。代码按帧号删除最小键,但没有拒绝过旧帧的水位线。这不是当前测试证明的错误,却是规模演进时应观察的边界:一旦乱序报告跨度超过两个检查点,有限容量与旧帧重建会相互作用。

最小演进方案不是立刻引入复杂窗口协议,而是增加 highest_seen_frame,拒绝低于可接受水位的报告,并记录淘汰原因。只有确实需要跨多个检查点容忍移动网络长尾时,才把固定两个改成基于时间预算的滑动窗口。

失败处置

形成多数时,系统至少有一个可用的责任代理;无多数时,任何单座位处罚都缺少依据。因此当前实现返回空的 kicked,只更新房间级 suspect。这是正确的降级方向:从“自动归责”退回“保留风险”,而不是为了保持动作一致硬选一个受害者。

房间进入可疑状态后的处置不应由仲裁器自行决定,因为产品模式不同,损失函数也不同。休闲房可以继续运行但不提交排名;竞技房可以暂停结算并保存回放;锦标赛可以立即终止并交给裁判流程;开发环境则应保存输入、随机种子、逻辑版本和首个分歧帧,以便离线重演。它们共享同一个检测事实,却需要不同业务策略。

正常时序是:所有当前在线座位提交同一检查点,严格多数形成,连续无多数计数清零,少数处罚按规则更新,检查点删除。失败时序则是:所有报告收齐但无严格多数,检查点删除,连续计数增加;第三次连续失败后,可疑标记保持为真,高层策略读取该状态并决定继续、冻结结果或终止。

恢复路径不能写成“下一帧一致就当没发生”。后续一致只适合恢复检测活性,不适合恢复比赛可信度。若本局结果仍有经济价值、排位影响或赛事奖励,可疑标记必须进入结算条件:在完成权威重演或人工规则确认前,结果不能直接转成不可逆资产。

错误归责

最危险的替代方案,是在无多数时处罚“与上一次多数不同”的座位。它假设历史多数代表持续真相,但版本切换、房间迁移或共同缺陷都可能让历史基线失效。更糟的是,两人房间一旦第一次随机选定基线,之后所有分歧都会稳定惩罚另一方,系统把第一次猜测固化成了权威。

处罚“最后上报者”同样站不住。报告顺序反映网络路径和调度,不反映确定性正确性;它会系统性伤害高延迟地区和移动网络玩家。处罚“哈希最少见者”在三人 1:1:1 中没有意义,在四人 2:2 中则退化为容器顺序。把这些启发式藏在代码里,只会让事故看起来像随机故障。

另一个过度方案,是为了解决无多数而引入分布式共识协议。共识协议可以让多个可靠节点对日志顺序达成一致,却不能证明客户端计算出的游戏状态符合规则。问题不是缺少更强的投票算法,而是缺少独立权威证据。若业务真的要求自动裁定,就应增加服务端重演或权威模拟,而不是把客户端票数包装得更复杂。

升级诊断

suspect 只是控制面信号,下一阶段最有价值的升级是诊断包,而不是更多处罚规则。诊断包至少应关联房间、检查点帧号、在线座位集合、各哈希计数、逻辑版本、随机种子和回放位置。这样,一个布尔状态才能被还原成可分析事件。

升级仍要保持边界:仲裁器输出结构化决定,房间层决定是否继续,回放层负责持久化,结算层执行奖励冻结。不要让 Arbiter 直接写数据库或调用风控服务,否则任何存储延迟都会进入战斗路径,单元测试也会被外部依赖淹没。

触发进一步演进的信号包括:可疑房间比例持续上升;无多数集中在特定逻辑版本;检查点淘汰率高,说明两个窗口不足;离线集合变化频繁导致检查长期不完整;可疑状态已影响有价值结算。对应动作依次是版本隔离、诊断聚合、调整可接受窗口、修复连接所有权,以及增加权威重演。

架构上最重要的判断是:无多数不是“裁决失败后仍要选一个答案”,而是一种必须被保留的知识状态。系统知道发生了分歧,也知道证据不足以归责。把这份不确定性显式建模,才能让实时路径保持简单,让更昂贵的诊断和业务处置只在真正需要时启动。