赛事玩法(Bracket / 瑞士轮 / BO 系列)
赛制建模 · 种子分配 · 比分晋级 · 录像回放 · 观战低延迟推流 · 反作弊闭环——用一套赛制骨架把电竞级玩法配置化上线。
一句话结论
赛事 = 赛制模型(Bracket / 瑞士轮 / 循环)+ BO 系列把胜负算清楚 + 种子分配让强者不打架 + 观战延迟预算(HLS ~10s / LL-HLS ~2s / WebRTC ~500ms)压反作弊窗口 + 确定性录像兜住重赛判定。匹配 / 反作弊 / 活动模板 / 网络低延迟四大能力靠交叉链接引用,本篇讲赛制骨架。
本篇的红线:不重复展开
- 匹配算法(MMR / ELO / Glicko / 扩圈搜索)详见 matchmaking,本篇只讲赛事场景下的种子分配与队伍对齐。
- 服务端权威与遥测评分详见 anti-cheat,本篇只讲赛事期加强层与重赛判定。
- 条件-进度-奖励三段抽象详见 activity-framework,本篇只讲赛事作为一种活动模板的接入方式。
- 端到端延迟预算与网络栈详见 net-lowlatency-fullstack,本篇只讲观战直播的延迟档位。
场景问题
普通匹配和赛事玩法看着都在"给玩家凑对手",其实是两回事:
| 维度 | 普通匹配(matchmaking) | 赛事玩法 |
|---|---|---|
| 目标 | 快 + 公平 | 公平 + 可观赏 + 可复现 |
| 参与人数 | 池子里的散人 | 已注册的固定队伍/选手 |
| 匹配方式 | MMR 扩圈搜索 | 按赛制模型(Bracket / 瑞士 / 循环)+ 种子 |
| 一局出结果 | 一句话 | BO3/BO5/BO7,含 Bye / Tie-Breaker / 断线补赛 |
| 观战 | 可选、不做也行 | 必选,且延迟档位有商业约束 |
| 录像 | 玩家自愿 | 必留,用于争议判定与转播 |
| 反作弊 | 常态 | 赛事期加强 + 重赛判定 + 申诉 |
三个真实的赛事事故
- BO5 断线补赛歧义:BO5 打到 2:2 决胜局,一位选手中途断线。规则说"重开当前局",但比分是保留还是清零?没写清就成撕逼。
- 观战延迟太短反噬反作弊:观战延迟 500ms 时,选手可能通过队友直播窥屏——这就是"直播延迟窗口"= 反作弊窗口的边界。
- 录像回放的"随机不复现":确定性回放依赖同一随机种子和同一输入序列。赛后申诉时录像跑出不同结果,判定成了黑箱。
赛事玩法开发的核心不是"再写一遍匹配代码",而是把赛制骨架做成配置化模板:赛制种类 + BO 层数 + 种子规则 + 观战档位 + 录像格式 + 反作弊策略 → 一次上线支持后续 N 种赛事。
实现方案
一:赛制模型对比
| 赛制 | 一句话 | 场次数 | 公平性 | 时长 | 典型场景 |
|---|---|---|---|---|---|
| 单败淘汰(Single-Elimination) | 输一场就出局 | N-1(N=选手数) | 低(1 败=出局,看运气) | 最短 | 8/16 强淘汰赛 |
| 双败淘汰(Double-Elimination) | 输一场进败者组,再输才出局 | ~2(N-1) | 高(给败者复活机会) | 中 | 电竞主流决赛 |
| 循环赛(Round-Robin) | 每对选手都打一场 | N(N-1)/2 | 最高 | 最长 | 联赛小组赛(8 队以内) |
| 瑞士轮(Swiss) | 每轮按当前战绩配对,同战绩避免重赛 | R 轮(通常 ⌈log₂N⌉+1) | 中高(不用全打完) | 中 | 大规模选手(棋类、卡牌、CS 起源) |
| 积分赛(League) | 长期积累,赛季末结算 | 可配置 | 高(大样本平均) | 最长 | 职业联赛全年 |
BO 系列(Best-of-N)是每场的层级,正交于赛制模型:
| BO 层数 | 决胜阈值 | 场次上限 | 典型 |
|---|---|---|---|
| BO1 | 1 胜 | 1 | 小组赛 / 大众预选 |
| BO3 | 2 胜 | 3 | 常规淘汰 |
| BO5 | 3 胜 | 5 | 半决赛 / 决赛 |
| BO7 | 4 胜 | 7 | 电竞顶级赛决赛 |
赛制正交组合
"单败淘汰 + BO3"、"双败淘汰 + BO5"、"瑞士轮 + BO3"——赛制模型和 BO 层数正交,赛事框架应支持同一场赛事内上下阶段用不同组合(如小组瑞士 BO1、淘汰双败 BO5)。
二:Bracket 数据模型
对局树 = 节点(Match)+ 胜/败方链接:
type Match struct {
ID uint64
Round int // 轮次(1 = 首轮)
Bracket BracketSide // WINNERS / LOSERS / FINAL
P1, P2 *Player // 双方;Bye 时其一为 nil
BestOf int // BO1/3/5/7
Score [2]int // 双方比分(局数)
Status MatchStatus // PENDING / IN_PROGRESS / DONE / WALKOVER
WinnerNext *Match // 胜者进入哪场
LoserNext *Match // 败者进入哪场(双败才有)
StartAt time.Time
ReplayIDs []uint64 // 关联录像 ID 列表
}
Bye(轮空)处理:选手数不足 2^k 时,前几名种子直接进下一轮。Bye 的对手 nil → Status = WALKOVER,不计入胜负记录,但必须留 Bracket 位(否则种子编号会错)。
Tie-Breaker(并列名次决胜):循环赛/瑞士轮的常见问题,按优先级依次比较:
- 直接对局胜负(Head-to-Head)
- 净胜局数(Game Win Ratio)
- 总局分(Points)
- 公共对手的强度(Buchholz / SOS,瑞士轮常用)
- 抛硬币 / 表演赛(真正最后手段)
断线补赛
- 局内断线(BO 内的一小局):约定"5 秒内重连则继续",否则该局判负;比分保留(重开的只是当前局)。
- 对局间断线(一小局已结束,下一局未开):延时 X 分钟后判该场剩余局全负;已打完的局比分保留。
- 不可抗力(服务器故障 / 断网):
WALKOVER或REPLAY_LATER,走申诉走复盘流程,不由选手主张。
关键:这三种边界必须赛前公示,事后再定就是撕逼。
三:种子分配
种子(Seeding)= 让强者不在早期相遇。三种典型:
| 方式 | 规则 | 优点 | 缺点 |
|---|---|---|---|
| Seed by rating | 按 MMR/积分从高到低编号,1 号种子对末号种子 | 强者晚遇 | 头部相互伤害风险大 |
| Snake seeding(S 型) | 分组时蛇形:A 组[1,4,5,8],B 组[2,3,6,7] | 分组均衡 | 计算稍复杂 |
| Random within tier | 分档次(Tier 1/2/3),档内随机 | 保观赏性 + 避免弱队反复被虐 | 需要合理档次线 |
跨赛区队伍的时区与延迟约束:
- 队伍所在赛区 → 服务器选择要能双向可接受(不能让一方 RTT 200ms+,详见 net-lowlatency-fullstack)。
- 时区差大的场次(如中美对阵)要在双方都不深夜的窗口安排;不行就先协商中立时区。
- 涉及签证/隔离期的线下赛还要预留 D-14 排期。
四:录像回放
确定性回放依赖:同一随机种子 + 同一输入序列 + 同一版本代码/数据。三者缺一,回放就"跑不出一样"。
- 格式:种子 + 版本 hash + 输入序列(按帧压缩)+ 关键帧快照(每 N 秒一份)。
- 快照点:既是回放起点(跳转 / 倍速播放的基础),也是灾难恢复点——一局打到一半服务端崩了,从最近快照回放 + 重放输入即可恢复(与 stateful-recovery 的 checkpoint + WAL 同思路)。
- 跳转与倍速:从快照 A 直接跳到快照 B 之间的任意帧,靠回放引擎快进。倍速播放不改物理,只改渲染节奏。
- 隐私脱敏:观战版本要去掉聊天、语音、账号信息;比赛版本可保留但归档时脱敏。
帧同步 vs 状态同步 的录像取舍
- 帧同步(同步输入):录像文件极小(只存输入序列 + 种子),但必须代码/数据版本完全一致才能回放;一次热更就让老录像"失效"。
- 状态同步(同步状态):录像文件大(存关键帧全状态),但跨版本可放(服务端把状态还原到客户端渲染即可)。
- 赛事一般选帧同步 + 版本冻结:赛期内代码不允许热更,录像永久可回放;平时用户版本用状态同步。
五:观战低延迟推流
观战不是"直播 + 拉流",是延迟档位与业务规则的交易:
| 档位 | 延迟 | 反作弊窗口 | 典型协议 | 适用 |
|---|---|---|---|---|
| 无延迟观战(服务端直接推) | ~50–200ms | 极小 | 私有 UDP | 赛事导播台内部 |
| 超低延迟直播 | ~500ms | 小 | WebRTC | 高价值付费观赛 |
| 低延迟直播 | ~2s | 中 | LL-HLS / CMAF | 主流直播 |
| 常规直播 | ~10s | 大 | HLS / DASH | 大众平台推流 |
观战延迟与反作弊窗口的悖论
观战延迟越短,"选手通过队友直播窥屏"的时间窗口越小,但推流成本、抖动/丢包容忍度、卡顿感都变差。所以:
- 官方直播通常主动加 10–30 秒延迟("直播延迟窗口" = 反作弊窗口);
- 导播台内部用无延迟观战做及时判断;
- 付费高观感套餐用 WebRTC 但限流量白名单,防止外挂利用超低延迟窥屏。
推流节点就近调度:详见 net-lowlatency-fullstack 的 GeoDNS / Anycast + BGP 章节,本篇不复述。
六:反作弊闭环
赛事期的反作弊在常态基础上加强,四步闭环:
- 常态:详见 anti-cheat(服务端权威、遥测评分、封禁申诉闭环)。
- 加强:赛事期开启关键操作服务端二次校验(比如"击杀判定"服务端复算而不是采信客户端)、录像强留、人工导播实时监控。
- 申诉窗:封禁后 24/48 小时可申诉,走独立评审组,避免误杀退赛。
- 赛后重赛判定:如果作弊被赛后确认(比如通过录像人工审核发现的开镜),要按赛前公示的规则判负 + 名次顺延或重赛。
七:作为一种活动模板接入
赛事本质是条件-进度-奖励三段抽象的一种实例(详见 activity-framework):
- Condition:报名条件(等级/段位/绑定要求)
- Progress:赛程进度(当前轮次 / 已完成场次 / 战绩)
- Reward:名次奖励、参与奖励、单场胜利奖励
复用活动框架的好处:幂等发奖、断线补发、防重复领取、多渠道对账这些通用能力不需要重写。
为什么这么做
- 为什么把赛制模型和 BO 层数拆成两层正交:真实赛事经常"小组瑞士 BO1、淘汰双败 BO5、决赛 BO7"多种组合叠加。拆开后配置化上线一次赛事只是配几个参数,不用重写代码。
- 为什么种子必须走 MMR/积分而不是随机:随机分配头部选手会导致 1 号种子第一轮就撞 2 号种子,头部相互伤害、观赏性崩盘。种子的意义就是用赛前信息把强者晚遇作为约束。
- 为什么录像必须确定性回放:赛后申诉需要"客观回放"这一板凳。若回放跑出不一样的结果,判定成黑箱,权威性崩塌。
- 为什么观战要主动延迟 10–30 秒:反作弊的边界就是"选手看不到实时对手信息"。观战延迟越短,作弊窗口越小;但官方直播主动加大延迟是最便宜的反作弊手段之一。
- 为什么赛事复用活动框架:报名、发奖、进度、公示、防重复——这些能力在活动侧已经沉淀过一遍,赛事再自造轮子只会踩同样的坑(重复发奖、少发奖、断线补发丢失)。
为什么别的选择不行
- 只用普通匹配代替赛制:匹配面向"池子里的散人",赛事面向"已注册的固定队伍"。前者要"快",后者要"公平 + 可观赏 + 可复现",目标函数不一样。
- 单败淘汰做重要赛事:一败即出局,头部选手被冷门爆冷就整场赛事看点全无。双败给败者一次复活机会,是电竞主流决赛选择。
- BO1 做决赛:偶然性太大,观众和选手都不服。决赛用 BO5/BO7 拉长样本,降低方差。
- 观战延迟不加:作弊窗口太大,被利用一次就是重大公关事故。
- 录像用状态同步:文件大 10-100 倍、成本爆炸;且状态同步的"跨版本可回放"优势在赛期(版本冻结)用不上。
- 赛事期不加强反作弊:常态反作弊的评分阈值是"错杀 vs 漏杀"的平衡;赛期"漏杀"的代价是奖金,必须调高召回宁可增加人工复审。
三个赛前必须公示的规则
- 断线补赛规则:局内断线、局间断线、不可抗力三种边界的判定;
- Tie-Breaker 顺序:哪个指标优先,抛硬币是不是终极手段;
- 申诉窗口与判定权限:申诉多长时间内有效,评审组是谁,是否可能重赛。
赛前不公示,事后必撕逼。
沉淀结论
- 赛事骨架 = 赛制模型(Bracket / 瑞士 / 循环)× BO 系列,两层正交组合配置化上线。
- 种子分配让强者晚遇:Seed by rating、Snake seeding、Random within tier 三种选一,避免头部互伤。
- Tie-Breaker 优先级:直接对局 → 净胜局 → 总局分 → Buchholz → 表演赛,必须赛前公示。
- 录像:确定性回放(种子 + 版本 + 输入序列),赛事期锁版本用帧同步。
- 观战延迟是反作弊窗口:官方直播主动加 10–30s,导播台用无延迟,付费白名单用 WebRTC。
- 赛事反作弊在常态基础上:关键操作服务端复算 + 录像强留 + 人工监控 + 申诉/重赛闭环。
- 接入 activity-framework 复用报名/发奖/进度/公示能力,不自造轮子。
记忆口诀
赛制两层:模型(单败/双败/循环/瑞士/积分)× BO(1/3/5/7)——正交组合
Bracket 三要素:Match 节点 / 胜败方链接 / Bye + Walkover 保位
Tie-Breaker 五级:直接对局 → 净胜局 → 总局分 → Buchholz → 表演赛
种子三法:Seed by rating / Snake seeding / Random within tier
观战四档:50ms 导播 / 500ms WebRTC / 2s LL-HLS / 10s HLS
反作弊四步:加强遥测 → 服务端复算 → 人工复审 → 判负/重赛/申诉
赛前三公示:断线补赛边界 / Tie-Breaker 顺序 / 申诉窗与权限——不公示必撕逼
录像三要素:随机种子 + 输入序列 + 版本冻结——缺一回放不复现
内容来源
综合整理。参考方向:Riot Games / Valve / Blizzard 官方电竞赛制文档、Challonge / Toornament 等赛事平台的数据模型公开信息、瑞士轮 Buchholz 分数在国际象棋联合会(FIDE)的定义、直播协议 HLS / LL-HLS / WebRTC 的延迟规范,以及站内 matchmaking / anti-cheat / activity-framework / net-lowlatency-fullstack 的赛事场景应用视角。
自测:合上资料能说清楚吗?
Q1: 一次八强淘汰赛,双败 + BO5,选手 8 位;打完 Winners Finals 后 Bracket 长什么样?
- Round 1(胜者组):4 场 BO5(1 对 8、4 对 5、2 对 7、3 对 6,按种子)
- Round 2(胜者组 = Winners Semifinals):2 场 BO5,四位胜者相遇
- Winners Finals:1 场 BO5,胜者进 Grand Final
- 输者进入 Losers Bracket,与前一轮的败者相遇,输一场就 DQ;最终 Losers Finals 胜者进 Grand Final
- Grand Final:胜者组冠军 vs 败者组冠军,通常 BO5,且胜者组冠军带 1:0 优势(因为它一路没输)
关键数据结构:每个 Match 有 WinnerNext / LoserNext 两个链接,胜者进胜者组下一场、败者进败者组对应场。
Q2: 瑞士轮 16 人,几轮打完?为什么可以少打?
⌈log₂16⌉ + 1 = 5 轮。瑞士轮的核心:每轮按当前战绩配对,同战绩之间打——⌈log₂N⌉ 轮之后战绩已经充分区分(数学上等价于二分收敛),无需像循环赛一样全打完 N(N-1)/2 = 120 场。Buchholz(对手强度总分)用于同战绩之间的 Tie-Breaker。
Q3: BO5 打到 2:2,决胜局一位选手断线,怎么判?
按赛前公示的规则:
- 局内断线(决胜局刚开 30 秒断的):约定 5 秒内重连则继续,否则该局判负 → 比分变 2:3,对方胜。已打完的 2:2 保留,不清零。
- 服务器故障:
REPLAY_LATER,走申诉复盘,不由选手主张。
关键在赛前公示:规则事前定好,事后按规则动作。规则不公示,事后再定就是撕逼。
Q4: 为什么官方直播主动加 30 秒延迟?不加更好看不好吗?
"直播延迟窗口" = 反作弊窗口。观战延迟越短,选手通过队友直播窥屏、外挂利用直播信息作弊的窗口越小。但直播延迟太短会:
- 推流成本高(WebRTC 每路成本远高于 HLS);
- 卡顿感明显(网络抖动更容易感知);
- 给作弊留窗。
主动加 10–30 秒延迟是最便宜的反作弊手段——外挂能拿到的信息永远比选手本人晚 30 秒。导播台用无延迟给解说、付费高观感用 WebRTC 白名单、大众直播用 HLS 10s 兜底,是三档并行的方案。
Q5: 帧同步录像和状态同步录像各有什么优劣?赛事选哪种?
- 帧同步录像:只存随机种子 + 输入序列(每帧压缩),文件极小(一局几十 KB);但必须代码/数据版本一致才能回放,一次热更让老录像失效。
- 状态同步录像:存关键帧全状态(每 N 秒一份),文件大 10–100 倍;但跨版本可回放(服务端还原状态到客户端渲染即可)。
赛事选帧同步 + 版本冻结:赛期内代码不允许热更,录像永久可回放,同时享受小文件优势。平时的用户版本用状态同步跨版本更灵活。
Q6: 赛事反作弊比日常反作弊多做什么?
在常态(服务端权威 + 遥测评分 + 封禁申诉,详见 anti-cheat)之上:
- 关键操作服务端二次校验:击杀判定、伤害计算、道具使用都在服务端复算(不采信客户端);
- 录像强留 + 人工监控:每局录像全留、导播台实时监控可疑动作;
- 判罚阈值调高召回:宁可多几个人工复审,也不能漏杀;
- 申诉与重赛闭环:24/48 小时申诉窗,赛后作弊确认要按规则判负 + 名次顺延或重赛。