还记得吗?任何复杂的程序,无论多么庞大,都是由三种控制结构(Control Structure)搭建起来的。在上一单元(边际森林第一章)里,你已经熟练掌握了其中两种——顺序和初步的循环。这一单元,你将解锁第三种、也是最能体现"智能"的一种:选择(Selection)。
| 结构 | 含义 | 代表语句 | 掌握进度 |
|---|---|---|---|
| 顺序 Sequence | 代码从上到下逐行执行 | — | ✅ 已学 |
| 循环 Iteration | 重复执行某段代码 | while True: | ✅ 已学 |
| 选择 Selection | 根据条件决定是否执行 | if | ⭐ 本单元 |
顺序结构有一个"硬伤":它太死板了。不管发生什么,它都会一行行照做。但真实的森林充满了不确定性——前方那间林地小屋里到底有没有食人魔?那个 X 标记处这次会不会刷出敌人?顺序结构无法应对这种"看情况而定"的局面。
这时就需要程序具备"判断"和"决策"的能力:先看一看情况,再决定做不做某件事。这正是选择结构的使命。
选择结构(Selection Structure),也叫分支结构(Branching),它允许程序先检验一个条件,再根据条件成立与否,决定执行哪一段代码。就像走到一个岔路口,先看看路标,再决定往哪边走。
其实你的生活里到处都是"if 判断"。比如:
注意这些句子里都有一个共同的模式:"如果……那么……"。而且,如果条件不成立,对应的动作就不做——不下雨就不用带伞。把这个日常逻辑翻译成 Python,就是本单元的主角:if 语句。
单分支选择结构是最简单的一种选择结构。"单分支"的意思是:它只有一个分叉口——条件成立时,执行一段代码;条件不成立时,什么都不做,直接跳过,继续往下走。
用一句话概括它的逻辑就是:"如果条件成立,就做某件事;否则跳过。" 它没有"否则该做什么"的分支(那是后面才学的双分支 if/else),所以叫"单分支"。
单分支结构最精髓的地方,就在于它允许程序"什么都不做"。在此之前,你写的每一行代码都一定会执行。但有了 if,代码块里的内容变成了"有条件的"——它可能执行,也可能被跳过。
"鹰眼"关卡里有个绝妙的比喻:渔夫放线钓鱼后,总会先判断有没有鱼上钩,确认有鱼才提竿收线。设想一下,如果什么都没钓到就把空线拉上来装进桶里,那不是很滑稽吗?英雄打怪也一样——得先确认有敌人,才挥剑攻击,否则就是"对着空气挥剑"。
在CodeCombat里,新的编程宝典 II(Programmaticon II)赋予了你使用 if 语句的能力。它的基本语法是:
if 条件: # 当条件成立时,执行这里缩进的代码 要做的事情
它由四个部分组成:关键字 if、一个条件、一个冒号 :,以及下面缩进的代码块。
| 组成部分 | 作用 | 示例 |
|---|---|---|
if | 关键字,表示"如果" | if |
| 条件 | 要检验的内容 | enemy |
: | 冒号,表示代码块开始 | : |
| 缩进代码 | 条件成立才执行 | hero.attack(enemy) |
if 后面、条件之后,必须紧跟一个冒号 :。冒号是 Python 在说:"注意,下面要开始一个代码块了。"漏写冒号是初学者最常见的语法错误之一。
冒号下面的代码必须向右缩进(通常是 4 个空格)。这些缩进的代码才"属于"这个 if,只有在条件成立时才会执行。一旦缩进结束、代码回到顶格,就表示 if 管辖的范围结束了。
if enemy: hero.attack(enemy) # 缩进:属于 if,有敌人才打 hero.attack(enemy) # 缩进:同样受条件控制 hero.moveXY(49, 51) # 顶格:不属于 if,无论如何都会执行
if 后面的条件,最终都会被 Python 判断为两个值之一:True(真)或 False(假)。这两个值叫做布尔值(Boolean)。条件为 True 时,执行代码块;为 False 时,跳过代码块。
你会发现 if 和 while 长得很像——都是"关键字 + 条件 + 冒号 + 缩进代码"。但它们有一个关键区别:
| 对比项 | if 语句 | while 循环 |
|---|---|---|
| 检验次数 | 检验一次 | 反复检验 |
| 执行次数 | 条件成立则执行一次 | 条件成立则反复执行 |
| 形象比喻 | "如果……就做一次" | "只要……就一直做" |
简单说:if 是"看一眼,做不做就这一次";while 是"一直盯着,只要条件还成立就不停地做"。
布尔值是以数学家乔治·布尔(George Boole)命名的,它只有两个取值:True 和 False。它们是计算机做一切判断的基础——可以把它们想象成电灯的"开"和"关"两种状态。
True 和 False 的首字母必须大写,而且它们是关键字,不能用作变量名。
本单元里,你最常用的 if 用法是检查某个东西是否存在。只需要在 if 后面直接写上那个变量名即可。例如:
enemy = hero.findNearestEnemy() if enemy: # 如果 enemy 确实指向一个敌人…… hero.attack(enemy)
当 enemy 里真的有一个敌人时,if enemy: 就为真,攻击会执行;当附近没有敌人时,enemy 是一个"空值",if enemy: 为假,攻击就被跳过。
当 findNearestEnemy() 附近没有任何敌人时,它返回的不是一个敌人,而是一个特殊的"空值"——在 Python 里通常是 None。None 在 if 判断中相当于 False。这就是为什么"没有敌人"时 if enemy: 会自动跳过的原因。
enemy 为真 → 执行攻击;"没敌人" → enemy 是 None(假)→ 跳过攻击。你不必手动写 == True,直接 if enemy: 就够了。
本单元的核心实战技能,可以总结为一套固定的"打敌人三件套"。无论哪一关,遇到"可能有敌人也可能没有"的情况,都用这三步搞定:
# 第①步:找到最近的敌人,存进变量 enemy = hero.findNearestEnemy() # 第②步:用 if 判断敌人是否存在 if enemy: # 第③步:如果存在,就攻击它 hero.attack(enemy)
| 步骤 | 代码 | 作用 |
|---|---|---|
| ① 找 | enemy = hero.findNearestEnemy() | 寻找最近的敌人 |
| ② 判 | if enemy: | 检查到底有没有找到 |
| ③ 打 | hero.attack(enemy) | 攻击这个敌人 |
hero.findNearestEnemy() 会让英雄环顾四周,找出距离最近的那个敌人。但找到之后,我们得把它"记下来",否则下一步就没法引用了。记下来的方法是用一个变量(Variable)把它存起来:
enemy = hero.findNearestEnemy() # 把找到的敌人,存进名叫 enemy 的变量里
等号 = 的意思是"赋值"——把右边的结果装进左边的盒子。这里 enemy 就是盒子的标签,findNearestEnemy() 的结果(一个敌人,或者 None)被装了进去。之后我们就能用 enemy 这个名字反复引用它。变量名可以自己起,比如村庄守护者关卡里就用了 leftEnemy 和 rightEnemy 来分别记住左右两边的敌人。
三件套里,第二步的 if 判断是最关键、也最容易被新手忽略的一步。它的作用是:在挥剑之前,先确认 enemy 里真的有一个敌人。有,才打;没有(None),就跳过。
正是因为有了这一步,"像这样使用 if 语句的话,就不会出现没有敌人还去攻击的错误了!"
确认有敌人后,用 hero.attack(enemy) 发动攻击。注意括号里要填上攻击谁——也就是把第①步存起来的 enemy 变量作为参数传进去。
有些关卡(如林地小屋、边地伏击战)里,敌人比较强壮,一次攻击打不死,所以会连续写两行 hero.attack(enemy) 来确保击败它:
if enemy: hero.attack(enemy) # 砍第一下 hero.attack(enemy) # 再砍一下,确保击败
在像"边地伏击战""清除"这样的关卡里,敌人是每次点击提交按钮时随机生成的——你根本无法预测哪个 X 标记处会刷出食人魔。如果不用 if 检查就盲目攻击,当某处恰好没有敌人时,英雄就会"对着空气挥剑",浪费时间甚至导致出错。
if 判断是不可省略的安全锁。
这一节我们换一种方式——不再贴出完整代码,而是聚焦每一关的难点、思路和策略。看懂了"为什么这样设计",你自然就能自己写出完整的解法。
这是你第一次使用 if 的关卡。地图上有几间固定的小屋,每间里可能藏着食人魔,也可能空着。难点不在语法,而在于意识到"每个地点都要独立检查一次"。
核心策略是把"移动 → 找 → 判 → 打"看成一个完整的"动作单元",每到一间小屋就完整地走一遍这个单元。这一关地点是固定的、数量有限,所以用顺序结构把这个动作单元重复书写即可,不需要循环。
最容易被忽略的一点是:每换一个地点,都必须重新调用一次 findNearestEnemy()。来看这个片段的关键逻辑:
hero.moveXY(19, 33) # 先到第一间小屋 enemy = hero.findNearestEnemy() # 在这里重新"看一眼" if enemy: # ……清剿……
enemy,英雄会一直盯着上一个地点的旧信息,导致判断出错。
"巡逻兵克星""野餐毁灭者""鹰眼""If 的盛宴"这几关,敌人会源源不断地出现。如果还像林地小屋那样把三件套抄很多遍,既写不完也应付不了无限刷新的敌人。
这里的思维跃迁是:用 while True 把三件套"包"起来,让它自动反复执行。这样只写一遍代码,就能不知疲倦地"找敌人 → 判断 → 攻击",直到清场。结构上的关键是两层缩进:
while True: # 第一层:循环 enemy = hero.findNearestEnemy() if enemy: # 第二层:判断(缩进一次) hero.attack(enemy) # 第三层:动作(再缩进一次)
这一关的挑战在于同时守护左右两个入口。策略是让英雄在循环里来回巡逻:先走到左入口检查清剿,再走到右入口检查清剿,如此往复。
这里有一个值得品味的设计:解法用了两个不同名字的变量 leftEnemy 和 rightEnemy。其实只用一个 enemy 也能过关,但用有意义的名字能让代码自我解释——一看就知道这是哪边的敌人。这是一个好的编程习惯。
findNearestEnemy() 来定义 rightEnemy。如果偷懒沿用左边的判断结果,英雄就会对右边来袭的敌人视而不见。"在能看到敌人从右侧过来时,确保你定义了 rightEnemy 变量。"
这一关最妙:它把"三件套"的第三步从攻击换成了建造火焰陷阱,并且引入了一个全新的两难——场上既有食人魔,又有想进村的农民。如果不分青红皂白地到处建陷阱,农民就会被误伤!
所以 if 在这里的意义发生了升华:它从"省得对空挥剑"变成了"分辨敌我、避免误伤"的安全开关——只有 findNearestEnemy() 真的发现了食人魔,才在对应位置建陷阱。结构上,三个方向(上/左/下)各是一个独立的"巡逻单元":
hero.moveXY(43, 50) # 走到"上"入口 top = hero.findNearestEnemy() if top: hero.buildXY("fire-trap", 43, 50) # left、bottom 两个方向,照此模式各写一组即可
while True,英雄就会在三个入口间永不停歇地巡逻布防。
"中点"是一道组合挑战,它要求你同时驾驭第一单元的 XY 坐标和本单元的 if 语句。英雄上下巡逻,在每个 X 标记处停下,看到食人魔才建陷阱——整体框架和荆棘农场一脉相承。
真正考验细心的,是一个容易被忽略的细节:"英雄站立的巡逻点"和"陷阱建造点"坐标并不相同。英雄站在通道上的 (32, …),却把陷阱建在敌人必经的 (35, …):
hero.moveXY(32, 52) # 英雄站在 x=32 的通道 topEnemy = hero.findNearestEnemy() if topEnemy: hero.buildXY("fire-trap", 35, 52) # 陷阱建在 x=35 的敌人通道
纵观本单元的所有关卡,if 和循环主要有两种组合方式,掌握它们就能应对绝大多数场景:
| 模式 | 结构 | 适用场景 | 代表关卡 |
|---|---|---|---|
| 模式 A | 顺序排列多组三件套 | 地点固定、数量有限 | 林地小屋、清除 |
| 模式 B | while 循环里套三件套 | 需要持续巡逻防守 | 巡逻兵克星、村庄守护者、荆棘农场 |
:——if enemy 后面少了冒号。findNearestEnemy(),攻击的还是旧敌人。rightEnemy,使用时却写成 enemy。恭喜你完成边际森林第二单元!你已经让程序拥有了"判断"和"决策"的能力,从只会埋头执行,进化到了能"看情况而定"。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 选择结构 | 程序的第三种基本结构 | 全部 |
| 单分支 | 条件成立才执行,否则跳过 | 全部 |
| if 语句 | if 条件: + 缩进代码 | 林地小屋 |
| 冒号与缩进 | 别忘冒号,缩进定归属 | 全部 |
| 条件判断 | True / False / None | If 的盛宴 |
| ① findNearestEnemy | 找到并存入变量 | 全部 |
| ② if enemy | 检查敌人是否存在 | 全部 |
| ③ attack | 把敌人当参数攻击 | 巡逻兵克星 |
| 多变量 | leftEnemy / rightEnemy | 村庄守护者 |
| if + buildXY | 看到敌人才建陷阱 | 荆棘农场、中点 |
会判断、会决策的英雄,才是真正聪明的英雄。
继续守护边际森林吧,勇敢的程序员!🌲🛡️
CodeCombat 编程知识 · 边际森林:第二单元
CodeCombat教研团队