Files
mcm-26-b/docs/协议规则表与状态机.md
T
2026-09-11 03:32:05 +08:00

216 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 协议规则表、字段表与状态机(P0 交付)
依据:`issue/附件/附件1.docx`《模拟器使用说明》、`issue/附件/附件2.docx`《模拟器通信接口说明及编程指南》。
本文是人类可读版本;**可执行版本**是 `src/mcm26b/protocol/rules.py`(字段表与错误语义)、
`src/mcm26b/protocol/timing.py`(计时模型)与 `src/mcm26b/config.py`(全部常量),
三者由 `tests/test_protocol_timing.py`、`tests/test_protocol_client.py` 固化验收。
## 1. 动作与耗时规则表
| 动作 | 指令 | 虚拟耗时 | 说明 |
| --- | --- | --- | --- |
| 开始 | `POST /enter` | 0 | 初始位置 (0,0),初始频道 1;虚拟/程序时钟开始 |
| 检测 | `POST /measure` | 5 + 移动 + 换频道 | 无论结果(no_signal/near/direction)均为 5s |
| 清除 | `POST /clear` | 未发现 3 / 成功 5 + 移动 | 5 = 光学定位 3 + 激光清除 2;**不切换测向频道** |
| 结束 | `POST /exit` | 0 | 成功响应中 exit_reason 固定 `user_exit` |
| 切换频道 | 无独立指令 | 1 | 只由合法 `/measure` 的 channel 变化推断 |
| 移动 | 无独立指令 | 直线距离/5(m/s) | 由 `/measure` 或 `/clear` 的 position 推断 |
派生事实(易错点):
- 一次合法 `/measure` 完成后,测向机频道更新为本次检测频道;
- `/clear` 的 channel 只是目标频道,既不切频道、也不产生切换耗时,因此
"清除后原地再测"的耗时只取决于上一次 **/measure** 的频道;
- 只有 accepted=true 的请求推进虚拟时钟;`accepted=false` 的 virtual_time_s=0 不是当前时刻;
- 5 秒检测只是虚拟耗时,不要求现实等待。
## 2. 字段表
公共请求字段(4 条指令都必填):
| 字段 | 类型 | 约束 |
| --- | --- | --- |
| arena_id | string | 必须为 `"default"` |
| robot_id | string | 逐字节等于登录参赛队号;UTF-8 长度 1..64;无控制字符 |
| request_id | string | 会话内幂等键;UTF-8 长度 1..128;无控制字符 |
动作字段(实现见 `rules.ACTION_FIELDS`):
| 指令 | 字段 | 约束 |
| --- | --- | --- |
| /enter | (无附加) | |
| /measure | position{x,y}, channel | x/y 有限数值且 \|v\|<=2,000,000;channel 整数 1..20(1.0 亦接受,1.5 拒绝) |
| /clear | position{x,y}, channel | 同上;channel 是目标频道 |
| /exit | (无附加) | |
响应字段:
| 场景 | 字段 |
| --- | --- |
| 所有响应 | accepted, real_timestamp_ms, virtual_time_s |
| accepted=false | 仅上述 3 个字段(virtual_time_s=0) |
| /enter 成功 | + max_virtual_duration_s, max_real_duration_s, remaining_real_duration_s |
| /measure 成功 | + measure_result ∈ {no_signal, near, direction};direction 时另有 svd_deg(保留两位小数) |
| /clear 成功 | + clear_result ∈ {success, no_target_in_range} |
| /exit 成功 | + exit_reason = "user_exit" |
错误语义(必须同时检查 HTTP 状态与 accepted):
| 情形 | 结果 | 对 request_id 的影响 |
| --- | --- | --- |
| JSON 语法/重复键/缺字段/类型/坐标频道范围 | HTTP 400 | 不占用,修正后可复用 |
| 未知字段(含 position 下)、arena/robot 不匹配、测试状态拒绝 | HTTP 200, accepted=false | 不占用 |
| 动作被接受 | HTTP 200, accepted=true | 占用 |
| 同 ID 不同内容,或并发不同动作 | HTTP 409 | — |
| 网络失败重试同一动作 | 复用原 ID+原内容,服务器回放首次响应 | 不重复生效 |
| 429/500 | 可原样重试 | — |
| 404/405/413/415 | 确定性错误,不得重试 | — |
物理规则速查:Ω 半径 1800m;源数 10–16;频道 1–20;接收半径 [1000,1500]m(逐源未知);
near 阈值 5m;清除半径 20m(与定向朝向无关);定向覆盖 = 朝向两侧各 90°(含边界);
示向度误差幅度 ±1°,保留两位小数;虚拟限时 360000s;程序限时 1200s(以 /enter 的
remaining_real_duration_s 为准,不得假定恒为 1200);测试窗口 25 分钟。
## 3. 状态机
机器狗会话状态(`protocol.state.RobotState` + 客户端会话标志):
```
/enter accepted /exit accepted
[未进入] ───────────────▶ [已进入] ───────────────▶ [已退出]
│ accepted=false │ accepted=false │ 任何动作 accepted=false
└── 不变 └── 不变 └── 不变
```
[已进入] 状态内跟踪:position、测向机 channel、virtual_time_s。转移规则:
| 事件 | position | channel | virtual_time_s |
| --- | --- | --- | --- |
| /enter accepted | (0,0) | 1 | 0 |
| /measure accepted | ← 本次 position | ← 本次 channel | ← 响应值(=旧值+移动+切换+5) |
| /clear accepted | ← 本次 position | **不变** | ← 响应值(=旧值+移动+3或5) |
| /exit accepted | 不变 | 不变 | ← 响应值 |
| accepted=false / 网络 / 400 | 不变 | 不变 | 不变 |
虚拟时刻以最近一次 accepted=true 的响应为准;本地预测(timing 模块)只用于
规划与一致性核对,偏差记入 `RobotState.mismatches` 并写日志(strict 模式抛错)。
## 4. 计时示例逐步推演(附件1表2 = 附件2第10节)
| 步 | 指令 | 移动 | 换频道 | 动作 | 本步 | 累计 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | /enter | 0 | 0 | 0 | 0 | **0** |
| 2 | /measure (300,400) ch1 | 500m→100s | 0(初始即1) | 5 | 105 | **105** |
| 3 | /measure (300,400) ch2 | 0 | 1 | 5 | 6 | **111** |
| 4 | /clear (300,0) 目标ch3 | 400m→80s | 0(clear不切频道) | 3(未发现) | 83 | **194** |
| 5 | /measure (300,0) ch2 | 0 | 0(第3步后已是2,clear未改) | 5 | 5 | **199** |
| 6 | /exit | 0 | 0 | 0 | 0 | **199** |
验收:`uv run mcm26b sim-check` 通过完整客户端→本地模拟器链路复现
`[105, 111, 194, 199]`;`tests/test_protocol_timing.py::TimingModelTests`
逐步断言每个分项。
## 5. 客户端设计要点(P1 交付)
- **串行**:完全同步,一个动作未决不发下一个;
- **幂等重试**:请求体一次性序列化为 bytes,网络失败/429/500 时原样重发
(同 request_id 同内容),最多 `max_attempts` 次,指数退避;400/404/405/409/413/415 立即失败;
- **accepted=false**:抛 `ActionRejected`,状态不更新;virtual_time_s=0 不入时钟;
- **现实预算**:/enter 成功后 `deadline = monotonic() + remaining_real_duration_s`;
新动作前检查 `剩余 > reserve_exit_s`(默认 60s [工程]),/exit 豁免;
- **日志**:`SessionLog` 逐事件追加 JSONL(attempt/transport_error/accepted),
进程崩溃不丢已发记录,回放测试与 evaluation 复算共用同一格式;
- **本地预校验**:channel 范围、坐标有限性与 2e6 上限、robot_id/request_id 格式
在发送前拒绝(与服务器 400 语义一致,省一次往返)。
## 6. 策略层如何使用本协议(P3/P4 交付)
第 5 节的客户端是策略层唯一的事实来源;`belief`(候选集)、`planner`(选点与闭环)
不绕开它直接发请求。衔接约定:
- **串行不变**:闭环每个决策步只发一个动作,`measure`/`clear` 的返回值
(`MeasureOutcome`/`ClearOutcome`)是 belief 更新的唯一输入;
- **状态同步**:机器人位置/测向频道/虚拟时刻一律读 `client.state`,
频道规则(§3 状态机表)由状态机自动维护,策略层不自行推算;
- **预算驱动退出**:`DeadlineExceeded` 只在 `/exit` 之外的动作前抛出;
闭环捕获后立即 `/exit`(豁免检查),并把该局记为"未证明完成",
不与全清成功混记;
- **选点的接收保证**:选点器只在保证接收盘 B(mec, 1000-ρ) 内取点,
盘内任一点到候选集每点距离 < 1000 ≤ 最小接收半径,因此定位阶段
不会浪费 `no_signal`(ρ>1000 的一般候选集退化为探索 MEC 圆心,允许
no_signal,理由字段中如实标注);
- **清除证书**:候选集外包多边形半径(含近似误差与工程余量)
≤ 20m 才发起 `/clear`;证书有效但清除失败视为异常,先补测再重试,
不在同一证书上重复消耗 3 秒/次的失败清除;
- **停止条件与完成判定**(2026-09-11 用户决策后):找全证明达成
(每频道均"已清除或覆盖证明不存在",或清除数达 16 数量上界)后
**不提前退出**:按 `search.persistent_search`(默认开)只复扫未清除
频道,直到清除数达 16、实机预算耗尽或虚拟安全余量
(`virtual_reserve_s`,防触及附件2 §4.5 的 100 小时上限;
本地模拟器对达限后的动作以 accepted=false 兜底)不足——"扫到限时为止"。
summary 中 `completed` 表示任务证明完成(无中断且结束时每频道
CLEARED/ABSENT,或 16 清除);`accounted_virtual_time_s` 记录首次
证明找全的虚拟时刻(计算"平均定位清除时间"建议用它,而非复扫
耗尽后的最终时刻);`stop_reason` 记录循环结束原因。发现若干源、
连续 no_signal、候选采样为空都不是停止条件;
- **每步计时留痕**(同上决策):客户端每个被接受动作写一条 `step`
事件(实机墙钟耗时 `real_elapsed_s`/`real_since_enter_s` + 比赛虚拟
耗时分项 `virtual_cost`:移动=直线距离/5、切频道 1s、检测 5s、
清除成功 5s/未发现 3s),summary 以 `steps` 数组与 `step_totals`
汇总,并给出题面口径的 `avg_locate_clear_time_s`;
- **验收**:`tests/test_loop_q3.py` 在自建模拟器上覆盖 10/16/0 源、
近边界源、预算耗尽与故障注入;`uv run mcm26b run --robot-id <队号>
--self-test` 可离线复跑随机场景,正式模式去掉 `--self-test` 即对接
模拟器(日志用 `--log` 落盘,格式同 §5 JSONL)。
## 7. 定向扩展如何使用本协议(P5 交付)
问题4的定向源在**协议字段上与全向源完全不可区分**:`/measure` 仍只返回
no_signal/near/direction(direction 附 svd_deg),`/clear` 仍只看 20m 半径。
"该源是定向的"无法从报文读出,只能由观测推理。因此 §5/§6 的协议用法全部
照用,定向性只改变策略层的解释与决策:
- **发现阶段零改动**:49 点覆盖保证对定向源同样成立——源位于某网格单元
凸包内时,任意发射半平面(朝向 ±90°,含边界)必含至少一个顶点,且该
顶点距离 ≤ 700√2 ≈ 989.95 < 1000 ≤ 最小接收半径,覆盖阶段无需知道
源类型;
- **no_signal 联合约束基线**:定向源的 no_signal 约束的是"位置×半径×朝向"
联合假设,单独维护空间候选会被误删,基线**不据此删任何候选**
(`belief.update_measure` 对 no_signal 无操作);缺席证明仍只由完整覆盖
给出。唯一启用的推理:若 no_signal 发生在保证接收盘 B(mec, 1000-ρ) 内
(该盘内任一全向源必然可收),则只能由朝向遮挡解释 [观测推理]——该频道
记为"观测到定向",进入探点升级梯子:对 MEC 圆心取对跖方向复核(偏移
裁进保证接收盘),再绕 MEC、半径 = ρ 的圆环按 45° 步进探点。**几何边界
必须如实陈述**:环上可见弧没有下界——源可位于环上(|g−c| = ρ)且朝向
沿外切线,此时可见集退化为单个切点,固定步进环探可以全部漏过。梯子
只提高提前命中概率,不构成接收或命中保证;完备性完全由后备清除兜底。
被遮挡探点不消耗定位步数,单轮被遮挡探点(首次选点、对跖复核、环探
合计)达 `max_blocked_probes`(4 [工程])即转后备清除;
- **后备清除的协议用法**:定位停滞(候选集有界但未达 §6 清除证书)时,
对候选外包多边形构造 20m 有限圆盘覆盖(`fallback_clear_cells`:全局
半开网格索引、相交判定带正容差,格内任一点距某格心 ≤ 10√2 < 20m),
按"距机器人最近优先"逐格发 `/clear`(同频道、不同位置)——清除与朝向
无关 [题面],是定向源不依赖可见性的完备后备。两条预算硬门槛(任一
不满足即跳过并留痕,绝不盲扫):
- 格数上界:N ≤ `fallback_max_cells`(400 [工程]);外包框枚举先按
(⌈w/20⌉+2)(⌈h/20⌉+2) 做规模检查,宽候选集(如仅一次读数的 750m
截断角域,数千格)必须继续定位而非硬扫;
- 现实预算:N × `real_rtt_p95_s` + `reserve_exit_s` ≤ 剩余现实时间。
虚拟耗时上界 ≈ 移动 L/5 + 3N(全失败)或 3(N-1)+5(末次成功)。
后备 /clear 请求**计入总请求口径**:summary 的 `total_requests`
(measures+clears)与 `fallback_clear_attempts`(后备逐格尝试数)
如实上报——仅统计检测次数会明显低估现实耗时;
- **真源不误删**:no_signal、失败的 `/clear`(含后备逐格全失败)均不删
空间候选;后备全失败按守恒性破坏记异常(格心距格内任一点 < 20m,
理论上必命中);
- **停止条件与预算语义不变**:仍是清除数达 16、每频道均"已清除或覆盖
证明不存在"、现实预算耗尽(记未证明完成)三种;断连/协议错误/
守恒性破坏中断时同样尽力 /exit 并产出未完成 summary(`stop_reason`
记 `transport_failure`/`protocol_error`/`inconsistent_belief` 等,
附逐频道状态快照),不静默丢失会话;
- **验收**:`tests/test_loop_q4.py`(15 项:覆盖格无空洞与格线两侧保留、
退化多边形、格数上界公式;混合 10/16 源全清、朝向背离圆心的近边界源
不漏、批量种子回归;no_signal 不删候选;格数硬顶与预算跳过;后备
正路径命中与全失败异常留痕);`uv run mcm26b run --robot-id <队号>
--problem 4 --self-test` 可离线复跑混合场景(`--directional` 指定
定向源数),正式模式同 §6。