第二单元:if 语句与打敌人三件套

Unit 2: The if Statement & the Combat Trio
边际森林 Border Forest · 林地小屋 → 中点

2.1 从顺序到选择

From Sequence to Selection

2.1.1 程序的三种结构(Three Control Structures)

2.1.1.1 复习:所有程序的"骨架" Review: the skeleton of all programs

还记得吗?任何复杂的程序,无论多么庞大,都是由三种控制结构(Control Structure)搭建起来的。在上一单元(边际森林第一章)里,你已经熟练掌握了其中两种——顺序和初步的循环。这一单元,你将解锁第三种、也是最能体现"智能"的一种:选择(Selection)

结构含义代表语句掌握进度
顺序 Sequence代码从上到下逐行执行✅ 已学
循环 Iteration重复执行某段代码while True:✅ 已学
选择 Selection根据条件决定是否执行if⭐ 本单元

2.1.1.2 为什么需要"选择" Why we need selection

顺序结构有一个"硬伤":它太死板了。不管发生什么,它都会一行行照做。但真实的森林充满了不确定性——前方那间林地小屋里到底有没有食人魔?那个 X 标记处这次会不会刷出敌人?顺序结构无法应对这种"看情况而定"的局面。

这时就需要程序具备"判断"和"决策"的能力:先看一看情况,再决定做不做某件事。这正是选择结构的使命。

2.1.2 什么是选择结构(What is a Selection Structure)

2.1.2.1 让程序学会"看情况" Letting programs react to conditions

CodeCombat 选择结构示意:根据条件走不同的路 - codecombat.cn
图1:选择结构让程序像走到岔路口一样,根据条件决定走哪条路

选择结构(Selection Structure),也叫分支结构(Branching),它允许程序先检验一个条件,再根据条件成立与否,决定执行哪一段代码。就像走到一个岔路口,先看看路标,再决定往哪边走。

2.1.3 生活中的选择(Selection in Daily Life)

2.1.3.1 你每天都在做"if 判断" You make "if" decisions every day

其实你的生活里到处都是"if 判断"。比如:

注意这些句子里都有一个共同的模式:"如果……那么……"。而且,如果条件不成立,对应的动作就不做——不下雨就不用带伞。把这个日常逻辑翻译成 Python,就是本单元的主角:if 语句。

2.2 单分支选择结构

Single-Branch Selection Structure

2.2.1 单分支的定义(Definition of Single-Branch)

2.2.1.1 只有一条"岔路" Only one branch

单分支选择结构是最简单的一种选择结构。"单分支"的意思是:它只有一个分叉口——条件成立时,执行一段代码;条件不成立时,什么都不做,直接跳过,继续往下走。

用一句话概括它的逻辑就是:"如果条件成立,就做某件事;否则跳过。" 它没有"否则该做什么"的分支(那是后面才学的双分支 if/else),所以叫"单分支"。

2.2.2 执行流程图(Flowchart)

2.2.2.1 一图看懂单分支 Understanding single-branch in one diagram

开始 条件成立? if 条件 是(True) 执行代码块 否(False) 继续往下
图2:单分支流程图——条件为真则执行代码块,为假则直接跳过

2.2.3 "做或不做"(Do or Skip)

2.2.3.1 关键在于"可以什么都不做" The key: it can do nothing

单分支结构最精髓的地方,就在于它允许程序"什么都不做"。在此之前,你写的每一行代码都一定会执行。但有了 if,代码块里的内容变成了"有条件的"——它可能执行,也可能被跳过

💡 渔夫钓鱼的故事

"鹰眼"关卡里有个绝妙的比喻:渔夫放线钓鱼后,总会先判断有没有鱼上钩,确认有鱼才提竿收线。设想一下,如果什么都没钓到就把空线拉上来装进桶里,那不是很滑稽吗?英雄打怪也一样——得先确认有敌人,才挥剑攻击,否则就是"对着空气挥剑"。

2.3 if 语句详解

The if Statement in Detail

2.3.1 if 语句语法(Syntax of if)

2.3.1.1 基本格式 Basic format

在CodeCombat里,新的编程宝典 II(Programmaticon II)赋予了你使用 if 语句的能力。它的基本语法是:

if 条件:
    # 当条件成立时,执行这里缩进的代码
    要做的事情

它由四个部分组成:关键字 if、一个条件、一个冒号 :,以及下面缩进的代码块。

组成部分作用示例
if关键字,表示"如果"if
条件要检验的内容enemy
:冒号,表示代码块开始:
缩进代码条件成立才执行hero.attack(enemy)

2.3.2 冒号与缩进(Colon and Indentation)

2.3.2.1 别忘了冒号 Don't forget the colon

if 后面、条件之后,必须紧跟一个冒号 :。冒号是 Python 在说:"注意,下面要开始一个代码块了。"漏写冒号是初学者最常见的语法错误之一。

2.3.2.2 缩进决定"谁属于 if" Indentation defines what belongs to the if

CodeCombat 缩进决定代码归属示意 - codecombat.cn
图3:缩进的代码才"属于"if,决定了哪些代码受条件控制

冒号下面的代码必须向右缩进(通常是 4 个空格)。这些缩进的代码才"属于"这个 if,只有在条件成立时才会执行。一旦缩进结束、代码回到顶格,就表示 if 管辖的范围结束了。

if enemy:
    hero.attack(enemy)   # 缩进:属于 if,有敌人才打
    hero.attack(enemy)   # 缩进:同样受条件控制

hero.moveXY(49, 51)  # 顶格:不属于 if,无论如何都会执行
常见错误:缩进必须统一。同一个代码块里,要么全用 4 个空格,要么全用 Tab,不能混用,否则 Python 会报错。

2.3.3 条件与布尔值(Conditions and Booleans)

2.3.3.1 条件的本质是"真"或"假" A condition is ultimately true or false

if 后面的条件,最终都会被 Python 判断为两个值之一:True(真)或 False(假)。这两个值叫做布尔值(Boolean)。条件为 True 时,执行代码块;为 False 时,跳过代码块。

2.3.4 if 与 while 的区别(if vs. while)

2.3.4.1 长得像,但行为不同 They look alike, but behave differently

你会发现 ifwhile 长得很像——都是"关键字 + 条件 + 冒号 + 缩进代码"。但它们有一个关键区别:

对比项if 语句while 循环
检验次数检验一次反复检验
执行次数条件成立则执行一次条件成立则反复执行
形象比喻"如果……就做一次""只要……就一直做"

简单说:if 是"看一眼,做不做就这一次";while 是"一直盯着,只要条件还成立就不停地做"。

2.4 条件判断

Conditions and Truthiness

2.4.1 真与假(True and False)

2.4.1.1 布尔值:编程世界的"是/否" Booleans: the yes/no of programming

布尔值是以数学家乔治·布尔(George Boole)命名的,它只有两个取值:TrueFalse。它们是计算机做一切判断的基础——可以把它们想象成电灯的"开"和"关"两种状态。

小提示:在 Python 中,TrueFalse 的首字母必须大写,而且它们是关键字,不能用作变量名。

2.4.2 检查存在性(Checking for Existence)

2.4.2.1 if 后面直接跟一个变量 Putting a variable right after if

本单元里,你最常用的 if 用法是检查某个东西是否存在。只需要在 if 后面直接写上那个变量名即可。例如:

enemy = hero.findNearestEnemy()
if enemy:     # 如果 enemy 确实指向一个敌人……
    hero.attack(enemy)

enemy 里真的有一个敌人时,if enemy: 就为真,攻击会执行;当附近没有敌人时,enemy 是一个"空值",if enemy: 为假,攻击就被跳过。

2.4.3 None 与空值(None and Empty Values)

2.4.3.1 "什么都没找到"是什么 What "nothing found" looks like

findNearestEnemy() 附近没有任何敌人时,它返回的不是一个敌人,而是一个特殊的"空值"——在 Python 里通常是 NoneNoneif 判断中相当于 False。这就是为什么"没有敌人"时 if enemy: 会自动跳过的原因。

理解要点:"有敌人" → enemy 为真 → 执行攻击;"没敌人" → enemyNone(假)→ 跳过攻击。你不必手动写 == True,直接 if enemy: 就够了。

2.5 打敌人三件套

The Combat Trio: Find, Check, Attack

2.5.1 三件套总览(Overview of the Trio)

2.5.1.1 安全打怪的黄金三步 The golden three steps of safe combat

CodeCombat 打敌人三件套:找敌人、判断、攻击 - codecombat.cn
图4:打敌人三件套——① 找敌人 ② 判断是否存在 ③ 攻击

本单元的核心实战技能,可以总结为一套固定的"打敌人三件套"。无论哪一关,遇到"可能有敌人也可能没有"的情况,都用这三步搞定:

# 第①步:找到最近的敌人,存进变量
enemy = hero.findNearestEnemy()
# 第②步:用 if 判断敌人是否存在
if enemy:
    # 第③步:如果存在,就攻击它
    hero.attack(enemy)
步骤代码作用
① 找enemy = hero.findNearestEnemy()寻找最近的敌人
② 判if enemy:检查到底有没有找到
③ 打hero.attack(enemy)攻击这个敌人

2.5.2 第一件:findNearestEnemy(Finding the Enemy)

2.5.2.1 找到最近的敌人并"记住"它 Find the nearest enemy and remember it

hero.findNearestEnemy() 会让英雄环顾四周,找出距离最近的那个敌人。但找到之后,我们得把它"记下来",否则下一步就没法引用了。记下来的方法是用一个变量(Variable)把它存起来:

enemy = hero.findNearestEnemy()
# 把找到的敌人,存进名叫 enemy 的变量里
💡 变量就像一个贴了标签的盒子

等号 = 的意思是"赋值"——把右边的结果装进左边的盒子。这里 enemy 就是盒子的标签,findNearestEnemy() 的结果(一个敌人,或者 None)被装了进去。之后我们就能用 enemy 这个名字反复引用它。变量名可以自己起,比如村庄守护者关卡里就用了 leftEnemyrightEnemy 来分别记住左右两边的敌人。

2.5.3 第二件:if enemy 判断(Checking with if)

2.5.3.1 这一步最关键,绝不能省 The most critical step — never skip it

三件套里,第二步的 if 判断是最关键、也最容易被新手忽略的一步。它的作用是:在挥剑之前,先确认 enemy 里真的有一个敌人。有,才打;没有(None),就跳过。

正是因为有了这一步,"像这样使用 if 语句的话,就不会出现没有敌人还去攻击的错误了!"

2.5.4 第三件:attack 攻击(Attacking)

2.5.4.1 把敌人当参数传进去 Pass the enemy as an argument

确认有敌人后,用 hero.attack(enemy) 发动攻击。注意括号里要填上攻击谁——也就是把第①步存起来的 enemy 变量作为参数传进去。

有些关卡(如林地小屋、边地伏击战)里,敌人比较强壮,一次攻击打不死,所以会连续写两行 hero.attack(enemy) 来确保击败它:

if enemy:
    hero.attack(enemy)   # 砍第一下
    hero.attack(enemy)   # 再砍一下,确保击败

2.5.5 为什么必须检查(Why the Check Matters)

2.5.5.1 敌人是随机出现的 Enemies appear randomly

CodeCombat 敌人随机出现,可能有也可能没有 - codecombat.cn
图5:每次点击提交,X 标记处可能有敌人,也可能空无一人

在像"边地伏击战""清除"这样的关卡里,敌人是每次点击提交按钮时随机生成的——你根本无法预测哪个 X 标记处会刷出食人魔。如果不用 if 检查就盲目攻击,当某处恰好没有敌人时,英雄就会"对着空气挥剑",浪费时间甚至导致出错。

血的教训:"没有敌人时不要攻击!别把英雄弄糊涂了。" 这就是为什么三件套里的 if 判断是不可省略的安全锁。

2.6 实战关卡精讲

Level Strategy & Analysis

这一节我们换一种方式——不再贴出完整代码,而是聚焦每一关的难点、思路和策略。看懂了"为什么这样设计",你自然就能自己写出完整的解法。

2.6.1 林地小屋(Woodland Cottage)

2.6.1.1 思路:把三件套当成"一个动作单元" Strategy: treat the trio as one action unit

CodeCombat 林地小屋关卡 - codecombat.cn
图6:林地小屋——移动到每个小屋附近,用三件套检查并清剿

这是你第一次使用 if 的关卡。地图上有几间固定的小屋,每间里可能藏着食人魔,也可能空着。难点不在语法,而在于意识到"每个地点都要独立检查一次"

核心策略是把"移动 → 找 → 判 → 打"看成一个完整的"动作单元",每到一间小屋就完整地走一遍这个单元。这一关地点是固定的、数量有限,所以用顺序结构把这个动作单元重复书写即可,不需要循环。

最容易被忽略的一点是:每换一个地点,都必须重新调用一次 findNearestEnemy()。来看这个片段的关键逻辑:

hero.moveXY(19, 33)          # 先到第一间小屋
enemy = hero.findNearestEnemy()  # 在这里重新"看一眼"
if enemy:
    # ……清剿……
为什么要重新找?因为换了地方,"最近的敌人"也变了。如果不更新 enemy,英雄会一直盯着上一个地点的旧信息,导致判断出错。

2.6.2 巡逻兵克星(Patrol Buster)系列

2.6.2.1 思路:从"顺序重复"升级到"循环复用" Strategy: from repetition to looping

"巡逻兵克星""野餐毁灭者""鹰眼""If 的盛宴"这几关,敌人会源源不断地出现。如果还像林地小屋那样把三件套抄很多遍,既写不完也应付不了无限刷新的敌人。

这里的思维跃迁是:while True 把三件套"包"起来,让它自动反复执行。这样只写一遍代码,就能不知疲倦地"找敌人 → 判断 → 攻击",直到清场。结构上的关键是两层缩进

while True:                       # 第一层:循环
    enemy = hero.findNearestEnemy()
    if enemy:                    # 第二层:判断(缩进一次)
        hero.attack(enemy)       # 第三层:动作(再缩进一次)
关键洞察:循环每转一圈,都会重新 find 一次,所以即使敌人是后面才刷出来的,下一圈也能被发现并击败。这就是"循环 + 判断"组合的强大之处。

2.6.3 村庄守护者(Village Guard)

2.6.3.1 思路:两个入口要用两个不同的变量 Strategy: two entrances need two variables

CodeCombat 村庄守护者关卡 - codecombat.cn
图7:村庄守护者——在左右两个入口之间来回巡逻,分别检查敌人

这一关的挑战在于同时守护左右两个入口。策略是让英雄在循环里来回巡逻:先走到左入口检查清剿,再走到右入口检查清剿,如此往复。

这里有一个值得品味的设计:解法用了两个不同名字的变量 leftEnemyrightEnemy。其实只用一个 enemy 也能过关,但用有意义的名字能让代码自我解释——一看就知道这是哪边的敌人。这是一个好的编程习惯。

最易出错的地方:移动到右入口后,必须重新 findNearestEnemy() 来定义 rightEnemy。如果偷懒沿用左边的判断结果,英雄就会对右边来袭的敌人视而不见。"在能看到敌人从右侧过来时,确保你定义了 rightEnemy 变量。"

2.6.4 荆棘农场(Thornbush Farm)

2.6.4.1 思路:if 不只用来攻击,还能用来"分辨敌我" Strategy: if can tell friend from foe

CodeCombat 荆棘农场关卡 - codecombat.cn
图8:荆棘农场——只在看到食人魔时建火焰陷阱,避免误伤农民

这一关最妙:它把"三件套"的第三步从攻击换成了建造火焰陷阱,并且引入了一个全新的两难——场上既有食人魔,又有想进村的农民。如果不分青红皂白地到处建陷阱,农民就会被误伤!

所以 if 在这里的意义发生了升华:它从"省得对空挥剑"变成了"分辨敌我、避免误伤"的安全开关——只有 findNearestEnemy() 真的发现了食人魔,才在对应位置建陷阱。结构上,三个方向(上/左/下)各是一个独立的"巡逻单元":

hero.moveXY(43, 50)        # 走到"上"入口
top = hero.findNearestEnemy()
if top:
    hero.buildXY("fire-trap", 43, 50)
# left、bottom 两个方向,照此模式各写一组即可
举一反三:看懂了 top 这一组,left 和 bottom 只需改坐标、换变量名就能照葫芦画瓢。把整套放进 while True,英雄就会在三个入口间永不停歇地巡逻布防。

2.6.5 组合挑战·中点(Midpoint)

2.6.5.1 思路:坐标与 if 的融会贯通 Strategy: combining coordinates and if

"中点"是一道组合挑战,它要求你同时驾驭第一单元的 XY 坐标和本单元的 if 语句。英雄上下巡逻,在每个 X 标记处停下,看到食人魔才建陷阱——整体框架和荆棘农场一脉相承。

真正考验细心的,是一个容易被忽略的细节:"英雄站立的巡逻点"和"陷阱建造点"坐标并不相同。英雄站在通道上的 (32, …),却把陷阱建在敌人必经的 (35, …)

hero.moveXY(32, 52)                # 英雄站在 x=32 的通道
topEnemy = hero.findNearestEnemy()
if topEnemy:
    hero.buildXY("fire-trap", 35, 52)  # 陷阱建在 x=35 的敌人通道
策略提示:规划时不妨问自己两个问题——"英雄站哪儿能看到这个方向的敌人?" 和 "陷阱建在哪儿才挡得住它?" 想清楚这两点,坐标就不会填错了。

2.7 if + 循环的威力

The Power of if Inside Loops

2.7.1 循环里的 if(if Inside while)

2.7.1.1 两种黄金组合模式 Two golden patterns

纵观本单元的所有关卡,if 和循环主要有两种组合方式,掌握它们就能应对绝大多数场景:

模式结构适用场景代表关卡
模式 A顺序排列多组三件套地点固定、数量有限林地小屋、清除
模式 Bwhile 循环里套三件套需要持续巡逻防守巡逻兵克星、村庄守护者、荆棘农场

2.7.2 常见错误(Common Mistakes)

2.7.2.1 新手最容易踩的坑 Pitfalls for beginners

易错点清单:
  • 忘写冒号 :——if enemy 后面少了冒号。
  • 缩进错误——攻击代码没有缩进,或缩进不一致。
  • 忘记重新 find——换地方后没重新调用 findNearestEnemy(),攻击的还是旧敌人。
  • 跳过 if 直接 attack——没敌人时"对空挥剑"。
  • 变量名写错——定义时叫 rightEnemy,使用时却写成 enemy

本单元小结 ✨

恭喜你完成边际森林第二单元!你已经让程序拥有了"判断"和"决策"的能力,从只会埋头执行,进化到了能"看情况而定"。来回顾一下核心概念:

核心概念关键点对应关卡
选择结构程序的第三种基本结构全部
单分支条件成立才执行,否则跳过全部
if 语句if 条件: + 缩进代码林地小屋
冒号与缩进别忘冒号,缩进定归属全部
条件判断True / False / NoneIf 的盛宴
① findNearestEnemy找到并存入变量全部
② if enemy检查敌人是否存在全部
③ attack把敌人当参数攻击巡逻兵克星
多变量leftEnemy / rightEnemy村庄守护者
if + buildXY看到敌人才建陷阱荆棘农场、中点

会判断、会决策的英雄,才是真正聪明的英雄。
继续守护边际森林吧,勇敢的程序员!🌲🛡️

CodeCombat 编程知识 · 边际森林:第二单元

CodeCombat教研团队