[
  {
    "id": "registration",
    "title": "学习目标与任务定义",
    "hours": "1.5",
    "weeks": "第 1 周",
    "goal": "把学习兴趣变成一周内能完成的小任务，写清要做什么、怎样判断完成，以及需要留下哪些记录。",
    "units": [
      [
        "（1） 机器人任务长什么样",
        "8 分钟",
        "“让机器人走一圈”还不是完整的巡检任务。你需要写清从哪里出发、检查哪里、哪些区域可以走，以及遇到封路时怎么办。机器人到达巡检点后，还要记录现场状态，任务才算完成。",
        "把“检查走廊”改写为：从 A 出发，到 B 记录一次状态，返回 A；遇到封路时报告失败并停止。分别写出需要提供的信息、要交出的结果，以及怎样判断任务完成。",
        "到达 B 但没有状态记录，算完成吗？",
        "不算。到达的位置只能说明走到了哪里；还要留下检查结果。"
      ],
      [
        "（2） 四种证据",
        "8 分钟",
        "代码告诉别人你打算怎样做，日志记录实际发生的事，指标方便比较结果，视频帮助还原现场。一张成功截图通常不够，还需要说明当时用了哪版代码、什么输入，以及怎样运行。",
        "为“机器人返回 A”各选一条日志、一项指标和一个视频片段，并写出它们分别能证明什么。",
        "一段视频能否证明算法在所有地图上可靠？",
        "不能。证据的范围不能超过这次实验的条件。"
      ],
      [
        "（3） 先写可以检查的目标",
        "10 分钟",
        "“掌握 Python”太宽泛，很难判断是否完成。“读入四行日志，正确计算路程和耗时”就具体得多。先确定本周要做出的成果，再选择需要学习的内容。",
        "在线填写一页项目卡：问题、输入、输出、验收条件；把本周目标限制在一项可以检查的成果。",
        "“读完三章”与“正确分析一份新日志”，哪项属于成果？",
        "后者更能说明学习成果。“读完三章”反映阅读进度；能否正确分析新日志，才需要通过实际结果检验。"
      ],
      [
        "（4） 时间减半，怎样调整计划",
        "10 分钟",
        "时间变少时，先缩小任务范围，保留一个能够完成并检查的结果。例如先写好异常检测函数，再考虑界面，不要把所有功能都只做一半。",
        "把 8 小时的计划缩减为 4 小时：保留一项必须完成的成果，去掉一项额外功能，并说明完成标准是否改变。",
        "删去测试来压缩时间，会失去什么？",
        "会失去确认结果是否正确的依据；优先缩小功能范围。"
      ],
      [
        "（5） 提交第一份任务卡",
        "10 分钟",
        "个人资料帮助老师了解你需要什么支持，任务卡用来记录学习目标和成果。阅读公开课程不需要先完成保密协议审批；涉及非公开项目资料时，再按项目的授权要求使用。",
        "在线填写任务卡和每周可投入时间；提交后阅读助教指出的一个模糊点，修改为新版本。",
        "任务卡符合要求，是否说明已经会操作机器人？",
        "不能。这一关只检查任务是否写清楚、需要的记录是否想明白。"
      ]
    ],
    "project": "为“走廊往返巡检”编写个人任务卡，并完成 8 小时到 4 小时的缩减实验。",
    "evidence": [
      "任务卡：输入、输出、约束、成功与失败条件",
      "两版时间计划及一项取舍理由",
      "一个尚不清楚的问题与所需帮助"
    ],
    "checks": [
      "必填项齐全，至少一项成果可以让别人核对",
      "工时之和不超过可投入时间",
      "助教追问后，学生能区分学习投入与实验成果"
    ],
    "variation": "把“走廊畅通”改为“B 点前封路”，补写失败报告应包含的字段。",
    "hint": "把“检查走廊”改写为：从 A 出发，到 B 记录一次状态，返回 A；遇到封路时报告失败并停止。分别写出需要提供的信息、要交出的结果，以及怎样判断任务完成。\n为“机器人返回 A”各选一条日志、一项指标和一个视频片段，并写出它们分别能证明什么。\n在线填写一页项目卡：问题、输入、输出、验收条件；把本周目标限制在一项可以检查的成果。\n把 8 小时的计划缩减为 4 小时：保留一项必须完成的成果，去掉一项额外功能，并说明完成标准是否改变。\n在线填写任务卡和每周可投入时间；提交后阅读助教指出的一个模糊点，修改为新版本。"
  },
  {
    "id": "toolkit",
    "title": "Python 编程与调试",
    "hours": "3",
    "weeks": "第 1 周",
    "goal": "在浏览器中运行和修改 Python 程序，检查计算结果，并记录一次从报错到修复的过程。",
    "units": [
      [
        "（1） 第一次在线运行",
        "8 分钟",
        "代码区写计算步骤，输入区放数据，输出区显示本次运行的结果。修改并保存代码后，还要重新运行；否则看到的仍是上一次的输出。",
        "在代码区保留本关两个函数，完成后调用 travel_distance(0.5, 4)，先预测结果为 2，再把输入区 seconds 改为 6 并重跑。尚未实现函数时，NotImplementedError 表示任务未完成，不是运行环境损坏。",
        "修改代码后输出仍是 2.0，先检查什么？",
        "检查是否重跑，以及当前输出是不是修改后的运行结果。"
      ],
      [
        "（2） 变量、类型与单位",
        "10 分钟",
        "0.5 是数值，\"0.5\" 是字符串。速度乘以时间可以得到路程，但单位必须对应。程序能运行，不代表计算结果一定正确；还要检查输入类型和单位。\n\n函数把可重复的步骤组织起来：def 定义函数，括号里是参数，return 返回结果，函数体统一缩进。下面只演示单位换算，还没有加入本关要求的输入检查。\n\n```python\ndef meters_from_mm(value):\n    return value / 1000\n\nassert meters_from_mm(500) == 0.5\n```",
        "先把字符串“500”转换为数值，再将毫米每秒换算为米每秒，计算 4 秒内的路程。from_mm 接收数值参数；字符串转换应在调用前完成。",
        "直接把 500 当成米每秒会得到多大误差？",
        "数值放大 1000 倍。应在读入数据时转换单位。"
      ],
      [
        "（3） 读懂一条报错",
        "10 分钟",
        "读报错时，先找异常类型和出错行，再检查这一行用到了哪些值。SyntaxError 通常表示语法有误，NameError 常见于变量未定义或拼写错误，TypeError 通常与类型不匹配有关。",
        "分别修复缺少冒号、变量拼写不一致和字符串参与数值相加的问题；每次只改一个原因。",
        "报错在第 8 行，原因一定只在第 8 行吗？",
        "不一定。错误值可能由更早的代码产生。沿变量来源检查。"
      ],
      [
        "（4） 用断言检查特殊输入",
        "12 分钟",
        "断言可以把预期结果变成反复执行的检查。除了正常输入，还要测试零、负数、空输入和 NaN 等特殊值。哪些值允许输入，要以本关的函数要求为准。\n\nPython 中 bool 是 int 的子类，因此要先排除布尔值，再检查数值类型、math.isfinite 和是否非负。非法输入用 raise ValueError 报告；assert 则适合把你预先算好的结果写成测试。",
        "实现 travel_distance(speed, seconds)：速度与时长必须为非负有限数；正常返回乘积，输入不合法时抛出 ValueError 异常。",
        "零秒是否一定是非法输入？",
        "本关要求允许零秒，返回零。不要拒绝本来允许的输入。"
      ],
      [
        "（5） 保存一次调试过程",
        "10 分钟",
        "记录一次调试时，写清出现了什么问题、你怀疑什么、怎样复现、改了哪里，以及重新测试的结果。只留下最终代码，别人无法了解你是怎样找到问题的。",
        "保留一次失败运行和一次修复运行；写下为什么这次修改能解释原报错。",
        "“换了一份能跑的代码”算排错解释吗？",
        "还不够。要说明原来为什么出错，以及这次修改为什么能解决问题。"
      ]
    ],
    "project": "编写一个速度—时间—距离计算器，完成单位换算与输入检查。",
    "evidence": [
      "可编辑 Python 代码与运行版本",
      "正常值、零值、负值、非有限值的测试记录",
      "一次失败到修复的前后对照与解释"
    ],
    "checks": [
      "0.5 m/s × 4 s = 2 m，零秒返回 0",
      "负数、NaN 和无穷数按要求拒绝",
      "修改代码后必须重新运行，旧测试结果标记为过期"
    ],
    "variation": "界面改为接收毫米每秒，但函数仍接收米每秒；说明转换应放在哪里。",
    "hint": "在代码区保留本关两个函数，完成后调用 travel_distance(0.5, 4)，先预测结果为 2，再把输入区 seconds 改为 6 并重跑。尚未实现函数时，NotImplementedError 表示任务未完成，不是运行环境损坏。\n先把字符串“500”转换为数值，再将毫米每秒换算为米每秒，计算 4 秒内的路程。from_mm 接收数值参数；字符串转换应在调用前完成。\n分别修复缺少冒号、变量拼写不一致和字符串参与数值相加的问题；每次只改一个原因。\n实现 travel_distance(speed, seconds)：速度与时长必须为非负有限数；正常返回乘积，输入不合法时抛出 ValueError 异常。\n保留一次失败运行和一次修复运行；写下为什么这次修改能解释原报错。"
  },
  {
    "id": "git-basics",
    "title": "Git 版本管理与协作",
    "hours": "3",
    "weeks": "第 2 周",
    "goal": "在个人教学仓库中练习 Git，学会查看改动、提交版本、合并分支和撤销错误修改。",
    "units": [
      [
        "（1） 三个位置",
        "10 分钟",
        "工作区里是正在编辑的文件，暂存区里是准备放进下一次提交的改动，提交记录则保存了一个版本。点击编辑器的“保存”，并不等于已经执行 Git 提交。",
        "在个人教学仓库中修改 README，再依次运行 git status、git diff、git add 和 git diff --cached，对比每一步的输出。",
        "修改文件后直接 git commit，为什么可能没有新内容？",
        "修改尚未进入暂存区。先确认差异，再暂存。"
      ],
      [
        "（2） 一次提交围绕一件事",
        "10 分钟",
        "提交记录要让别人看懂：为什么改、改了什么，以及哪次测试对应哪个版本。一次提交尽量围绕一件事，排错或撤销时就更容易找到对应的改动。",
        "分三次提交：任务目标、输入检查、复盘说明；每次运行测试后写一句有意义的提交信息。",
        "“update”能否说明修改目的？",
        "通常不足；“拒绝负时长，避免产生负路程”更便于追踪。"
      ],
      [
        "（3） 分支与冲突",
        "12 分钟",
        "分支记录不同的修改路线。两个分支改了同一段内容时，合并可能出现冲突。解决冲突要决定最后保留什么内容；删掉冲突标记后，还需要检查代码含义并重新测试。",
        "确认位于 main 分支，将 README 中“速度单位”一行改成自己的正确说明，保存并提交；再选择“合并同伴单位修改（peer-change）”。出现冲突后，编辑 README 保留一致的单位说明，删除冲突标记，保存、暂存并提交。practice 分支作为额外分支练习。",
        "冲突标记消失，是否代表合并正确？",
        "不代表；文件语义和测试仍可能出错。"
      ],
      [
        "（4） 找到引入错误的版本",
        "12 分钟",
        "用修改记录找到要撤销的提交，再对照自己保存的改动说明理解原因。git revert 会新增一个提交来撤销指定改动，保留原有历史；它并不删除出错版本。网页提供修改记录与撤销操作，不提供任意 Git 命令。",
        "仅在本关教学仓库中，将 travel.py 的乘法误改为除法并提交，记下该次提交编号。随后执行“撤销一次修改（git revert）”，核对文件恢复；把恢复后的函数放回第 2 关运行测试，记录结果。",
        "为何不直接把所有历史删掉？",
        "会丢失追踪和协作依据，也无法解释事故来源。"
      ],
      [
        "（5） 在线提交版本证据",
        "10 分钟",
        "提交作业时，除了仓库地址，还要注明具体的提交编号（修改记录中，提交说明前的那串字母和数字）。如果运行前还有未提交的改动，要保存这些差异；否则别人检出同一个版本，也可能无法得到你的运行结果。",
        "提交最终版本的提交编号、差异、图形历史和一次测试结果；比较运行时的代码与提交版本是否一致。",
        "工作区不干净时，运行结果能否直接归到 HEAD？",
        "不能；必须记录额外差异或先提交。"
      ]
    ],
    "project": "为第 2 关程序建立可复现版本历史，完成一次真实合并冲突与一次撤销。",
    "evidence": [
      "真实在线仓库的三次内容提交、分支与合并记录",
      "冲突前后差异和解决理由",
      "最终提交编号、该版本的代码、对应测试结果和一次撤销记录"
    ],
    "checks": [
      "记录来自真实 Git 进程，网页示意不能代替真实提交记录",
      "提交之间确有内容差异，最终运行对应所交版本",
      "学生能指出撤销的具体改动及恢复后的测试"
    ],
    "variation": "第二次合并引入单位错误，要求依据 diff 找到原因，并保留修复记录。",
    "hint": "在个人教学仓库中修改 README，再依次运行 git status、git diff、git add 和 git diff --cached，对比每一步的输出。\n分三次提交：任务目标、输入检查、复盘说明；每次运行测试后写一句有意义的提交信息。\n确认位于 main 分支，将 README 中“速度单位”一行改成自己的正确说明，保存并提交；再选择“合并同伴单位修改（peer-change）”。出现冲突后，编辑 README 保留一致的单位说明，删除冲突标记，保存、暂存并提交。practice 分支作为额外分支练习。\n仅在本关教学仓库中，将 travel.py 的乘法误改为除法并提交，记下该次提交编号。随后执行“撤销一次修改（git revert）”，核对文件恢复；把恢复后的函数放回第 2 关运行测试，记录结果。\n提交最终版本的提交编号、差异、图形历史和一次测试结果；比较运行时的代码与提交版本是否一致。"
  },
  {
    "id": "python-basics",
    "title": "机器人日志分析",
    "hours": "5",
    "weeks": "第 2–3 周",
    "goal": "从机器人日志中计算路程、耗时和速率，找出值得检查的异常区间，说明现有数据能支持哪些判断。",
    "units": [
      [
        "（1） 一行日志能告诉你什么",
        "10 分钟",
        "每行日志包含时间 t_s、位置 x_m 和 y_m，以及电量 battery_pct。先确认字段、单位和时间顺序，再计算指标。时间必须严格递增；缺字段、NaN 或重复时间都应明确报错，不能直接当作零处理。\n\nrows 是由字典组成的列表，row[\"t_s\"] 读取一行的时间。可用下面的循环取出相邻两行；循环前仍需完成字段和时间检查。\n\n```python\nfor previous, current in zip(rows, rows[1:]):\n    dt = current[\"t_s\"] - previous[\"t_s\"]\n```",
        "观察样例四行数据，手算每段耗时；在在线数据表插入一条重复时间，说明为何分段速率无法计算。",
        "t = 1 到 t = 1 能否通过给分母加很小数“修复”？",
        "不能。这掩盖了数据质量问题，应报告行号和原因。"
      ],
      [
        "（2） 路程不等于位移",
        "12 分钟",
        "路程描述沿运动轨迹经过的长度；位移描述终点相对起点的位置变化。在线练习把相邻采样点用直线连接，累加得到路程估计，采样稀疏时可能漏掉中间的弯曲轨迹。走出去再回到原点，路程大于零，位移为零；二维点间距离可用 hypot(dx, dy) 计算。",
        "用 (0,0) → (3,4) → (0,0) 手算路程与位移，再补全代码，让程序计算每段距离并累加。这里按题目给出的坐标计算，不必套用实验区的样例。",
        "这条轨迹的路程和位移分别是多少？",
        "路程 10 m，位移大小 0 m。"
      ],
      [
        "（3） 采样间隔不同时，怎样计算平均速率",
        "12 分钟",
        "全程平均速率等于总路程除以总耗时。如果各段耗时相同，可以直接平均各段速率；耗时不同，就要按时长加权，或直接用总路程除以总耗时。",
        "对 t=[0,1,3]、x=[0,1,1]、y=[0,0,0] 计算两段速率与全程平均速率；构造能揭露错误实现的断言。",
        "结果是 0.5 还是 1/3 m/s？",
        "全程为 1/3 m/s；0.5 是两段速率的未加权平均。"
      ],
      [
        "（4） 发现异常后，能得出什么结论",
        "12 分钟",
        "速度超过阈值，表示这段记录值得进一步检查，不足以直接确定故障原因。位置跳变可能来自定位重置、时间记录错误，也可能是真实运动。阈值应为大于零的有限数。",
        "使用 t=[0,1,2]、x=[0,0,10] 检出 1–2 s 区间。列出需要补查的两种资料，例如原始传感器时间与重定位事件。",
        "检测到 10 m/s，能否直接判定电机故障？",
        "不能。应报告可疑区间、阈值和待验证假设。"
      ],
      [
        "（5） 从参考结果到自己的实验",
        "15 分钟",
        "跑通样例后，再用静止、往返、斜线和不同采样间隔的数据检查程序。每次先写下预期，再运行并比较，这样才能判断代码是否真正按你的理解工作。",
        "完成下方 Python 练习作业，提交自建数据、手算值、运行结果与一次失败的修复。",
        "测试全通过但无法解释平均速率，是否正式通过？",
        "说明仍需补充；助教应追问并允许修订。"
      ]
    ],
    "project": "实现 summarize(rows, threshold)，形成可复现的巡检日志分析报告。",
    "evidence": [
      "summarize 的个人实现和代码版本",
      "标准测试、异常输入与自建日志的运行记录",
      "正常指标、可疑区间、推断边界和排错复盘"
    ],
    "checks": [
      "浮点指标绝对误差不超过 0.001；使用总路程/总时间",
      "非法时间、非有限输入及无效阈值有明确错误",
      "至少一个自建反例，不能只使用参考数据",
      "学生解释路程/位移、平均速率、异常与根因的区别"
    ],
    "variation": "保持位置相同但改变采样时间，预测平均速率与最大分段速度如何变化。",
    "hint": "观察样例四行数据，手算每段耗时；在在线数据表插入一条重复时间，说明为何分段速率无法计算。\n用 (0,0) → (3,4) → (0,0) 手算路程与位移，再补全代码，让程序计算每段距离并累加。这里按题目给出的坐标计算，不必套用实验区的样例。\n对 t=[0,1,3]、x=[0,1,1]、y=[0,0,0] 计算两段速率与全程平均速率；构造能揭露错误实现的断言。\n使用 t=[0,1,2]、x=[0,0,10] 检出 1–2 s 区间。列出需要补查的两种资料，例如原始传感器时间与重定位事件。\n完成下方 Python 练习作业，提交自建数据、手算值、运行结果与一次失败的修复。"
  },
  {
    "id": "ros2-simulation",
    "title": "ROS 2 仿真：指令与反馈",
    "hours": "6",
    "weeks": "第 3–4 周",
    "goal": "先预测圆形轨迹，再运行真实 turtlesim 仿真，用实际收到的 Pose 数据检查半径、方向和误差。",
    "units": [
      [
        "（1） 指令不是状态",
        "10 分钟",
        "速度命令表示希望小乌龟怎样运动，位置反馈表示实际观察到的状态。发出命令后，还要检查是否收到反馈。节点、话题名称和消息类型能帮助你确认程序之间是否连通。",
        "在本页“真实 ROS 2 仿真”中运行原速圆，在结果区找到运行编号、参数、节点与话题信息，并查看实际轨迹；需要留档时导出 pose.csv 和 result.json。",
        "已经发送 cmd_vel（速度指令），却没有收到 pose（位置反馈），能证明机器人到达了吗？",
        "不能。发出指令并不等于已经到达；缺少反馈也不能说明机器人一定没有移动。先检查反馈和时间记录，再判断实际状态。"
      ],
      [
        "（2） 认识发布者和订阅者",
        "12 分钟",
        "发布者发送 Twist 速度消息，订阅者接收 Pose 并记录时间。收不到消息时，先核对话题名称、消息类型和命名空间，再判断是否是程序逻辑的问题。",
        "根据本次结果中的节点与话题信息，标出速度命令的发布端和位置反馈的来源。具备独立 ROS 2 环境时，可选做订阅话题写错的排查练习；网页不提供任意终端命令。",
        "能看到话题名称，却收不到消息，还应检查哪些地方？",
        "检查类型、命名空间、发布者是否活跃及兼容的通信配置。"
      ],
      [
        "（3） 用模型预测圆轨迹",
        "12 分钟",
        "在理想平面运动中，线速度 v 恒定、角速度 ω 非零时，圆的半径为 |v/ω|，周期为 2π/|ω|。仿真采用离散步长，实际记录通常会有少量偏差；需要把预测值和实测值放在一起比较。",
        "先预测 v=1、ω=1 与 v=0.5、ω=1 的半径和周期，再分别运行记录。",
        "线速度减半、角速度不变，哪个量不变？",
        "理想周期不变，半径减半。该结论依赖模型条件。"
      ],
      [
        "（4） 采集实际运行的仿真反馈",
        "15 分钟",
        "用公式生成的坐标是预测数据，订阅到的 Pose 才是实际仿真反馈。每次运行都要保留编号、参数、CSV 和轨迹截图，避免混淆不同实验的结果。",
        "分别运行原速圆和半速圆，先在页面查看两次结果，再按需保存 Pose CSV 与轨迹。比较预测半径、实测轨迹和闭合误差；修改采样频率属于独立 ROS 2 环境中的选做练习。",
        "只有理想圆轨迹数组，是否完成本关 ROS 2 实验？",
        "没有。那只完成了数学预习，需要真实 ROS 2 节点与反馈。"
      ],
      [
        "（5） 停止与故障恢复",
        "12 分钟",
        "实验结束时，程序会发送零速度命令并结束仿真会话；运行器也设置了超时上限。网页上的状态文字只是提示，判断是否停止还要看执行端的状态或日志。",
        "检查本次运行的状态和结束日志，说明零速度指令与实际停止记录有什么区别。网页不提供停止指令丢失或记录器退出的故障注入；这些项目可先写验证方案，有独立环境后再实施。",
        "网页显示“停止”是否就是停止证据？",
        "需要检查执行端的状态或反馈，并保留停止时的记录，不能只看网页上的按钮文字。"
      ]
    ],
    "project": "先完成圆形轨迹预测，再用网页预设实验采集实际反馈并比较结果。通信排错、采样频率调整和故障恢复作为独立 ROS 2 环境中的选做扩展。",
    "evidence": [
      "真实云端 ROS 2 节点与话题信息",
      "两次 pose 日志、参数、轨迹及误差对照",
      "本次运行的结束状态与停止日志；通信排错和故障恢复为独立环境中的选做扩展。"
    ],
    "checks": [
      "运行环境为 ROS 2，数学仿真单独标记",
      "日志来自订阅器；参数变化与观察能对应",
      "对照本次运行公布的几何检查结果，说明预测值、实测值及差异；未公布的精度阈值不作为额外门槛。仿真结果不能替代真实机器人的精度评估。"
    ],
    "variation": "拓展思考：角速度改为负值时，方向和周期怎样变化？具备独立 ROS 2 环境时，可修改记录频率，核对采样密度与运动指令的区别。",
    "hint": "在本页“真实 ROS 2 仿真”中运行原速圆，在结果区找到运行编号、参数、节点与话题信息，并查看实际轨迹；需要留档时导出 pose.csv 和 result.json。\n根据本次结果中的节点与话题信息，标出速度命令的发布端和位置反馈的来源。具备独立 ROS 2 环境时，可选做订阅话题写错的排查练习；网页不提供任意终端命令。\n先预测 v=1、ω=1 与 v=0.5、ω=1 的半径和周期，再分别运行记录。\n分别运行原速圆和半速圆，先在页面查看两次结果，再按需保存 Pose CSV 与轨迹。比较预测半径、实测轨迹和闭合误差；修改采样频率属于独立 ROS 2 环境中的选做练习。\n检查本次运行的状态和结束日志，说明零速度指令与实际停止记录有什么区别。网页不提供停止指令丢失或记录器退出的故障注入；这些项目可先写验证方案，有独立环境后再实施。"
  },
  {
    "id": "mini-project",
    "title": "任务规划与异常恢复",
    "hours": "6",
    "weeks": "第 4–5 周",
    "goal": "用广度优先搜索（BFS）在栅格地图上寻找最短路线，正确处理无路可走的情况，并说明巡检任务还需要哪些环节。",
    "units": [
      [
        "（1） 先说明地图和输入要求",
        "10 分钟",
        "本关地图中 0 表示可走，1 表示障碍；每次只能向上、下、左、右移动一步，且每步代价相同。在这些条件下，BFS 能找到步数最少的路径。机器人尺寸、连续运动和动态避障暂不在本题范围内。",
        "编辑实验区的地图 JSON，设置起点、终点和障碍；先手画一条合法路径，说明坐标顺序使用 row、col。",
        "如果允许斜着走，原来只允许上下左右移动的最短路线还能直接使用吗？",
        "不能。允许的移动方向变了，程序和测试要求也要一起调整。"
      ],
      [
        "（2） 队列与访问标记",
        "12 分钟",
        "队列让搜索按距离起点的层次展开。格子加入队列时就标记为已访问，可以避免重复加入。用 parents 记录每个格子从哪里来，找到终点后才能还原路径。\n\n例如起点为 (0,0) 时，先将它加入队列并令 parents[(0,0)] = None。取出该点后，把合法且未访问的邻格加入队尾。访问标记针对地图格子；最终路径需要从终点沿 parents 回溯。",
        "手动推演 3×3 空地图从 (0,0) 开始的队列变化；完成代码中的队列弹出与邻居过滤。",
        "访问顺序就是最终路径吗？",
        "不是。许多访问点不在最终路径上。"
      ],
      [
        "（3） 路径重建与无解",
        "12 分钟",
        "找到终点后，沿 parents 回到起点，再把顺序反转，就得到完整路径。如果找不到路，返回 None，不能给出穿过障碍的路线。起点和终点相同时，路径只包含起点，步数为零。",
        "补全路径重建；测试起终点相同、终点被障碍包围与超出边界三种情况。",
        "起点等于终点时步数是多少？",
        "0；路径可为仅含起点的列表。"
      ],
      [
        "（4） 变化地图实验",
        "15 分钟",
        "不要只用开发时的一张地图测试。增加障碍、封住通道或移动终点，检查新路径是否仍合法、步数是否最少。同一张地图可能有多条正确的最短路径，不要求与参考路线逐点相同。",
        "用 5×5 空地图从 (0,0) 到 (4,4) 得到 8 步；封住整列 2 后报告无解；保留一个缺口后重跑。",
        "路径与参考不同但同长，是否错误？",
        "不一定。应检查连通、避障、端点与最短步数，而非逐点硬匹配。"
      ],
      [
        "（5） 从规划到巡检",
        "12 分钟",
        "规划回答的是“往哪里走”。真正完成巡检，还需要控制、定位、采集数据，以及在失败时停止并报告。写项目说明时，要分清已经实现的功能和还没有解决的问题。",
        "将“规划成功→执行→采集→返回”写成状态转移；加入“无路→停止并报告”，说明触发条件。",
        "BFS 单元测试通过，能否直接在矿井部署？",
        "不能。还缺地图质量、机器人尺寸、控制、定位和现场验证等条件。"
      ]
    ],
    "project": "完成可测试的栅格规划器与巡检任务状态图，记录一次成功找到路线和一次无路可走的结果。",
    "evidence": [
      "个人 BFS 实现、地图与对应运行",
      "正常/无解/边界测试以及两份变化地图",
      "任务状态图、一页项目卡，以及接入真实机器人前还需要补齐的功能"
    ],
    "checks": [
      "路径端点正确、每步四邻接、全程避障",
      "5×5 空地图从 (0,0) 到 (4,4) 的最短路径为 8 步；整列封路明确无解",
      "无解不能被当作执行成功；解释 BFS 的成立条件"
    ],
    "variation": "在开始执行后封住前方通道，设计何时停止、重新规划以及何时上报失败。",
    "hint": "编辑实验区的地图 JSON，设置起点、终点和障碍；先手画一条合法路径，说明坐标顺序使用 row、col。\n手动推演 3×3 空地图从 (0,0) 开始的队列变化；完成代码中的队列弹出与邻居过滤。\n补全路径重建；测试起终点相同、终点被障碍包围与超出边界三种情况。\n用 5×5 空地图从 (0,0) 到 (4,4) 得到 8 步；封住整列 2 后报告无解；保留一个缺口后重跑。\n将“规划成功→执行→采集→返回”写成状态转移；加入“无路→停止并报告”，说明触发条件。"
  },
  {
    "id": "graduation",
    "title": "项目一：ROS 2 乌龟控制",
    "hours": "3",
    "weeks": "第 6 周",
    "goal": "独立控制小乌龟移动、转向、调节线速度与角速度并停止。已有基础的同学可直接完成 ROS 2 挑战，获得前六关免修资格；普通路线继续整理前六关成果并复现一个关键结果。",
    "units": [
      [
        "（1） 直通车：独立控制小乌龟",
        "15 分钟",
        "有基础的同学直接进入本关在线实验，自行填写 7 段 Twist 指令。先直行并加快线速度，再原地左右转向，接着边走边转并加快角速度，最后主动停止。指令在真实 turtlesim 中执行，按实际 Pose 的位置、朝向与速度检查；不是运行三种预设轨迹。",
        "独立填写各段 linear.x 和 angular.z，运行后解释线速度、角速度及停止检查。",
        "通过直通车是否意味着前六关都已学习完成？",
        "通过后记录“前六关免修”，原有学习进度不改成已完成。该挑战验证 ROS 基本控制，Python、Git 等能力仍在后续课程检验。"
      ],
      [
        "（2） 普通路线：逐关对应目标与成果",
        "10 分钟",
        "逐关整理目标、代码或计划、实际结果和自己的解释。材料不必多，但要能对应各关要求。一张截图不能替代前六关的全部学习记录。",
        "列出六关状态和证据位置；缺失或未完成项如实标记为“进行中”。",
        "有六个文件是否就代表六关通过？",
        "不代表，文件内容还要符合各关要求。"
      ],
      [
        "（3） 普通路线：保存代码版本和实际运行记录",
        "12 分钟",
        "保存具体版本、运行命令、输入、输出和环境信息。第 5 关还要保存实际 turtlesim Pose、节点和话题信息，以及轨迹截图；理论预测单独标明。",
        "对照原速圆和半速圆，写出理论值与实际值的差异。",
        "用公式生成的 CSV 能代替实际 Pose 数据吗？",
        "不能。公式只能给出预测，无法证明 ROS 2 节点已经运行。"
      ],
      [
        "（4） 普通路线：检查清单与证据路径",
        "10 分钟",
        "在本关在线表单中填写前六关成果的位置或运行编号，逐项说明实际完成状态。检查材料能否打开、内容是否属于所写的版本和实验；未完成项如实保留。表单的填写检查不等于自动验证链接、文件存在或成果真实性。",
        "填写六关成果清单，实际打开其中一个记录，核对课程、版本和运行结果；发现缺失或访问受限时，补充说明并修订。",
        "结构检查通过能证明内容由本人完成吗？",
        "不能。当前表单只检查必填项是否填写；链接能否访问、材料是否真实以及是否达到课程要求，还需人工核对。"
      ],
      [
        "（5） 普通路线：让另一位成员复现",
        "12 分钟",
        "复现说明要写清版本、环境、输入、运行命令、预期结果和实际结果。分享链接前，确认同伴或老师有访问权限；非公开资料仍应限制在获授权的范围内。",
        "请同伴复现一个结果并记录差异；修订说明。",
        "只提供最新代码链接足够吗？",
        "不够，应固定版本并提供可复现步骤。"
      ],
      [
        "（6） 普通路线：三分钟演示与下一步",
        "10 分钟",
        "演示时，讲清目标、自己做的修改、结果、一次失败和下一步计划。使用过参考代码或 AI 帮助，也要如实说明。真实机器人实验由后续课程另行安排。",
        "提交反思、下一阶段目标、六关证据和三分钟演示。",
        "基础课程必须预约真机才能交付吗？",
        "不必；本关验收前六关成果，第 5 关要求实际运行的仿真，真实机器人实验属于后续扩展。"
      ]
    ],
    "project": "两条路线：已有基础的同学直接完成本关 ROS 2 独立操作挑战，系统检查真实运行记录并保存前六关免修资格；普通学习路线汇总前六关成果、完成复现实验并由导师验收。",
    "evidence": [
      "直通车：本人填写的 Twist 指令、实际 Pose、动作检查结果与操作说明。",
      "普通路线：前六关代码、计划、日志与实际检查结果",
      "普通路线：第 5 关实际运行 turtlesim 得到的仿真 CSV、节点/话题与轨迹",
      "普通路线：固定版本、环境、命令与同伴复现记录",
      "普通路线：一页反思、下一步目标、AI 使用说明与三分钟演示"
    ],
    "checks": [
      "直通车：7 段独立指令及实际 Pose 检查全部通过，保存操作说明和免修记录。",
      "普通路线：前六关实际达到各关要求，未完成项如实保留",
      "普通路线：证据路径可访问并对应具体版本与运行",
      "普通路线：第 5 关实际运行的仿真与理论轨迹明确区分，真机不是基础课程门槛",
      "普通路线：表单检查和 AI 点评不能代替导师验收"
    ],
    "variation": "选择一项填写不完整或访问受限的成果记录，说明缺失信息，补齐后重新检查；不要为了练习修改真实成果文件。",
    "hint": "直通车：完成 7 段独立 Twist 指令与真实 Pose 检查，保存操作说明。\n普通路线：整理前六关成果，完成一次复现与三分钟演示。"
  }
]

