去做题

第三单元:分支策略 · 让程序学会做决定

Unit 3: Branching Strategies — Teaching Your Program to Decide
计算机科学 5 · 安息云峰 Cloudrip Mountain · 收割火焰 → 劳心劳力

3.1 什么是分支策略

What Is a Branching Strategy

在第一单元里你学会了用 def 把行为封装成函数,让主循环变成一份清爽的行动清单。但真实的战斗从来不是"每帧都做同样三件事"这么简单——敌人有骑士、有巫师、有弓箭手,战场形势瞬息万变。这一单元你要让程序学会看情况做决定:先分析当前局势、算出一个"该采取什么策略"的结论,再根据这个结论去执行相应的行为。这就是分支策略(branching strategy)。

本单元主线:你将学会让一个函数用 return 返回一段代表策略的字符串(如 "griffin-rider"、"fight-back"),再用 if / elif / else 根据这个返回值分流去调用不同的函数;同时掌握 findByType 按类型精确筛选敌人、distanceTo 做距离判断,最后学会写带参数的函数,让士兵和弓箭手瞄准各自最合适的目标。

3.1.1 从"做一件事"到"看情况做"(From Doing to Deciding)

3.1.1.1 先决策,再执行 Decide first, then act

分支策略的核心,是把"我该怎么做"这个大问题拆成两小步:第一步决策——观察战场、算出一个结论;第二步执行——根据结论去做对应的动作。就像一位将军先派侦察兵摸清敌情("敌方骑士已经逼近雷区左侧!"),再据此下达命令("全军反击!")。程序里,"侦察摸清敌情"就是一个返回策略的函数,"据此下令"就是一串 if / elif / else。

分支策略:先算出"策略",再据此"执行" ① 决策函数 观察战场 chooseStrategy() return "策略字符串" 返回值 if 策略=="griffin-rider": 召唤 elif 策略=="fight-back": 反击 elif 策略=="collect-coins": 捡币

3.1.2 决策与执行分离(Separating Decision from Action)

3.1.2.1 为什么要分开 Why separate the two

把"决策"和"执行"分成两块,好处和第一单元"一个函数只做一件事"一脉相承:chooseStrategy 只负责思考、给出结论,具体每种策略怎么做则交给各自的函数(commandAttack、pickUpCoin、heroAttack)。将来想调整判断条件,只改决策函数;想改某种打法,只改对应的执行函数。二者互不干扰,代码清晰又好维护。

🧭 决策函数就像"导航仪"
决策(chooseStrategy):导航仪根据路况算出"下一步该左转"——它只给结论,不替你踩油门。
执行(if/elif/else 里的调用):你听到"左转"后,真正打方向盘、踩油门。
一个负责"想",一个负责"做",配合起来才能顺利到达终点。

3.2 函数返回策略字符串

Returning a Strategy String

3.2.1 return 关键字(The return Keyword)

3.2.1.1 让函数"交出一个结果" A function can hand back a value

第一单元的函数都是"做完就完",不给外面任何回信。这一单元的函数不一样:它要用 return 关键字把一个值交回给调用它的地方。比如 chooseStrategy() 想清楚了就 return "fight-back",这个字符串会顺着调用点传回来,被外面的变量接住:strategy = chooseStrategy()。此后 strategy 里装的就是 "fight-back"。

return 语句的两个零件 return "fight-back" 关键字 要交回的值(这里是字符串)

3.2.2 用字符串代表策略(Strings as Strategy Labels)

3.2.2.1 一个字符串 = 一种打法 One string stands for one plan

"收割火焰"关卡把三种打法各起了一个字符串名字:"griffin-rider"(有钱就召唤格里芬骑士)、"fight-back"(英雄亲自反击闯入雷区的骑士)、"collect-coins"(安全时去捡金币攒钱)。决策函数每帧观察一次战场,返回其中一个字符串。用字符串当"标签"的好处是可读性极高——看到 return "fight-back" 就知道此刻该反击,比返回 1、2、3 这种数字清楚得多。

# 决策函数:观察战场,返回一个策略字符串
def chooseStrategy():
    # 如果买得起格里芬骑士,就返回它的标签
    if hero.gold >= hero.costOf("griffin-rider"):
        return "griffin-rider"
    # ...其他判断...
    return "collect-coins"   # 兜底策略

3.2.3 return 立即结束函数(return Ends the Function)

3.2.3.1 一旦返回,后面的代码不再执行 return exits immediately

这是 return 一个非常重要、初学者常忽略的特性:只要执行到 return,函数立刻结束、把值交出去,它后面的代码一律不再运行。所以在决策函数里,把"最优先"的判断写在前面很关键——只要前面的条件满足并 return 了,后面的判断根本轮不到执行。这也让我们可以省掉很多 else:因为一旦 return,程序自然就不会走到下面。

碰到 return,函数当场"下班" if 买得起: return "griffin-rider" ← 满足! 立即交出值、退出 for ... : return "fight-back" ✗ 不会执行 return "collect-coins" ✗ 不会执行
顺序即优先级:决策函数里判断的书写顺序就是策略的优先级。"收割火焰"把"能召唤格里芬"放最前,是因为攒够钱扩军最划算;其次才是"有骑士闯雷区就反击";都不满足才去"捡金币"。想调整战术优先级,只要调换这几段判断的顺序即可。

3.3 if / elif / else 分流

Branching with if / elif / else

3.3.1 根据返回值分流(Dispatching on the Return Value)

3.3.1.1 拿到策略,对号入座 Match the strategy to an action

决策函数交回策略字符串后,主循环就用 if / elif / else 把它"对号入座":是 "griffin-rider" 就召唤、是 "fight-back" 就让英雄反击、是 "collect-coins" 就去捡币。这三条分支互斥——每帧只会走进其中一条,正好对应"当下只采取一种打法"。

while True:
    commandAttack()                 # 每帧都指挥友军攻击
    strategy = chooseStrategy()   # 拿到本帧策略
    # 根据策略分流,只走进其中一条分支
    if strategy == "griffin-rider":
        hero.summon("griffin-rider")
    elif strategy == "fight-back":
        heroAttack()
    elif strategy == "collect-coins":
        pickUpCoin()
if / elif / else 的分工:if 是第一道判断,elif(else if 的缩写)是"前面都不满足时再看这个",else 是"前面全不满足时的兜底"。它们从上到下依次检查,一旦某条成立就执行对应代码块,其余分支全部跳过——保证同一时刻只做一件事。

3.3.2 == 与 is 的比较(Comparing Strings)

3.3.2.1 判断字符串是否相等 Checking string equality

要判断策略字符串是不是某个值,标准写法是用相等运算符 ==:if strategy == "fight-back":。注意区分:= 是"赋值"(把右边装进左边),== 才是"比较是否相等",别把两者写混。

关于 is:扣哒世界 的部分关卡示例里用了 if strategy is "griffin-rider":。在这些教学关里它能"碰巧"工作,但在正规 Python 中,比较两个值是否相等应当用 ==;is 比较的是"是不是同一个对象",用来判断字符串内容相等并不可靠。养成好习惯:判断内容相等一律用 ==。
✅ 推荐:用 == 比较内容
if strategy == "fight-back":
    heroAttack()
⚠️ 关卡里出现但不推荐
if strategy is "fight-back":
    heroAttack()

3.4 筛选与距离判断

Filtering by Type & Judging Distance

3.4.1 findByType 按类型筛选(Filtering with findByType)

3.4.1.1 只挑出"某一类"单位 Pick out only one kind of unit

hero.findEnemies() 会返回所有敌人,但很多时候你只关心其中某一类——比如只想找"骑士 fangrider"、只想找"巫师 witch"。这时就用 hero.findByType("类型名"),它返回一个只含该类型单位的列表。把它和 findNearest 结合,就能精确锁定"最近的某一类单位"。

# 只找出敌方骑士,再从中挑最近的一个
fangriders = hero.findByType("fangrider")
nearestRider = hero.findNearest(fangriders)

# 也可以一行写完:找出所有巫师里最近的那个
nearestWitch = hero.findNearest(hero.findByType("witch"))
扣哒世界 findByType 按类型筛选敌人 - koudashijie.com
图1:findByType 像一张"筛网"——从混杂的敌群里只漏下你指定的那一类单位

3.4.1.2 遍历筛出来的列表 Loop over the filtered list

筛出来的是一个列表,可以用 for 逐个查看。"收割火焰"里判断"雷区左侧是否有敌方骑士",就是遍历所有 fangrider,看有没有谁的横坐标 pos.x 落在雷区范围内。这里也可以用 range(len(...)) 配下标访问,效果一样。

fangriders = hero.findByType("fangrider")
for i in range(len(fangriders)):
    fangrider = fangriders[i]
    if fangrider.pos.x < 38:       # 骑士已闯入雷区左侧
        return "fight-back"          # 立即决定反击

3.4.2 distanceTo 距离判断(Distance Checks)

3.4.2.1 够近才出手 Only strike when close enough

hero.distanceTo(目标) 返回英雄到目标的距离。攻击是有射程的,隔太远打不到,白白浪费动作。所以在英雄反击时,先用 distanceTo 判断距离,够近(例如 < 25 或 < 15)才 attack,否则先不动或移动靠近。这能避免"对着够不着的敌人挥空刀"。

def heroAttack():
    # 找到最近的敌方骑士
    enemy = hero.findNearest(hero.findByType("fangrider"))
    # 存在、且距离够近,才出手
    if enemy and hero.distanceTo(enemy) < 25:
        hero.attack(enemy)
先判断"存在"再判断"距离":写成 if enemy and hero.distanceTo(enemy) < 25:。and 左边先确认 enemy 不是空(确实找到了敌人),右边再算距离。若敌人根本不存在,左边为假就直接跳过,不会因为对"空目标"求距离而出错。这种"先确认存在、再进一步判断"的写法很常用。

3.5 针对性攻击

Targeting the Right Enemy

3.5.1 带参数的函数(Functions with Parameters)

3.5.1.1 给函数一个"输入" Passing information into a function

前面的函数括号都是空的,这一节的函数括号里要放一个参数。参数就是函数的"输入口":调用时你塞进去一个值,函数体内就能用这个名字来引用它。"劳心劳力"要求定义 chooseTarget(friend)——它接收一个 friend(某个友军单位),根据这个 friend 的兵种,返回它该攻击的目标。

带参数的函数头部 def chooseTarget ( friend ) : 参数(函数的输入口)
# friend 是参数:调用时传进来一个友军单位
def chooseTarget(friend):
    target = None
    # 函数体里可以直接用 friend 这个名字
    if friend.type == "archer":
        # ...
        pass
    return target

3.5.2 不同兵种不同目标(Different Units, Different Targets)

3.5.2.1 士兵打巫师,弓箭手打最近 Soldiers hit witches, archers hit nearest

"劳心劳力"的战术精髓,是让不同兵种各打各的目标:士兵(soldier)血厚、能扛,让它们冲上去缠住危险的女巫(witch);弓箭手(archer)脆但远程,让它们优先清掉离自己最近的任意敌人。判断依据就是 friend.type——用它区分这个友军是士兵还是弓箭手,再返回相应的目标。

🏹 弓箭手 archer
if friend.type == "archer":
    enemies = hero.findEnemies()
    target = friend.findNearest(enemies)
🛡️ 士兵 soldier
if friend.type == "soldier":
    witches = hero.findByType("witch")
    target = friend.findNearest(witches)
用 friend.findNearest 而非 hero.findNearest:注意示例里是 friend.findNearest(witches),而不是 hero.findNearest。因为我们要的是离这名友军自己最近的目标,而不是离英雄最近的。这样每个士兵会各自扑向离它最近的女巫,把士兵自然地分成几组,避免全挤在一处被女巫的溅射(范围伤害)一锅端。

3.6 实战关卡精讲

Level Strategy & Analysis

这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。

关卡 · 收割火焰

🔥 收割火焰 —— 让决策函数返回策略

扣哒世界 收割火焰关卡 - koudashijie.com
图2:收割火焰——指挥格里芬骑士飞越雷区保卫英雄,用一系列函数把复杂战斗拆成清晰的几块

策略:三种打法,看局势切换

本关目标是生存 30 秒并保住雷区完好至少 30 秒。食人魔骑士会试图冲过雷区,你既要攒钱召唤格里芬骑士扩军,又要在骑士闯入时亲自反击,安全时还得捡币维持经济。核心思路是写一个 chooseStrategy() 决策函数,每帧根据战场返回三种策略之一,主循环再据此调用不同函数。

思路拆解:决策函数的三段判断

决策函数按优先级从高到低写三段判断。第一,能扩军就扩军:if hero.gold >= hero.costOf("griffin-rider") 成立就 return "griffin-rider"。第二,雷区告急就反击:用 findByType("fangrider") 取出所有敌方骑士,遍历检查有没有谁的 pos.x 已经越过雷区边界(如 < 38),有就 return "fight-back"。第三,都不满足就攒钱:函数最后 return "collect-coins" 兜底。因为 return 会立即结束函数,前面满足了就轮不到后面,天然形成优先级。

# 关键片段:决策函数返回策略字符串
def chooseStrategy():
    # ① 买得起格里芬 → 扩军优先
    if hero.gold >= hero.costOf("griffin-rider"):
        return "griffin-rider"
    # ② 有骑士闯入雷区左侧 → 反击
    fangriders = hero.findByType("fangrider")
    for i in range(len(fangriders)):
        if fangriders[i].pos.x < 38:
            return "fight-back"
    # ③ 都不满足 → 捡币攒钱(兜底)
    return "collect-coins"

思路拆解:主循环按返回值分流

主循环里先 commandAttack() 让已有友军持续攻击,再 strategy = chooseStrategy() 拿到本帧策略,然后用 if / elif / elif 分流:"griffin-rider" 就 hero.summon(...),"fight-back" 就 heroAttack(),"collect-coins" 就 pickUpCoin()。每个执行函数各司其职:commandAttack 遍历友军各打最近敌人,heroAttack 结合 distanceTo 判断够近才砍闯入的骑士,pickUpCoin 移动到最近金币处。

while True:
    commandAttack()
    strategy = chooseStrategy()
    if strategy == "griffin-rider":
        hero.summon("griffin-rider")
    elif strategy == "fight-back":
        heroAttack()
    elif strategy == "collect-coins":
        pickUpCoin()
为什么 commandAttack 放在分流之外?因为"指挥友军攻击"是每帧都要做的事,和三选一的策略无关。把它放在 if 之前无条件执行,保证格里芬骑士时刻都在战斗;而召唤、反击、捡币这三件事才是"看情况三选一"的分支。分清"总要做的"和"看情况做的",是组织主循环的关键。
别忘了兜底 return:如果决策函数前面的条件都不满足却没有最后那句 return "collect-coins",函数会返回 None,主循环的 if/elif 一条都匹配不上,英雄这一帧就什么都不做、白白浪费时间。永远给决策函数一个兜底返回值。
关卡 · 劳心劳力

🧙 劳心劳力 —— 带参数的目标选择函数

扣哒世界 劳心劳力关卡 - koudashijie.com
图3:劳心劳力——食人魔女巫加倍麻烦,士兵专缠女巫、弓箭手清剿最近敌人,分工作战

策略:分兵专攻,避开溅射

本关登场的女巫(witch)攻击带范围溅射,友军挤成一团很容易被一锅端。破局思路是让不同兵种分工瞄准不同目标:士兵(soldier)冲上去各自缠住离自己最近的女巫,把队伍自然拆成几个小组分散站位;弓箭手(archer)则远程清掉离自己最近的任意敌人。这样既压制了危险的女巫,又避免了扎堆挨溅射。

思路拆解:定义 chooseTarget(friend)

核心是写一个带参数的 chooseTarget(friend) 函数,根据传入友军的 friend.type 返回它该打的目标。若是 "archer",就在所有敌人里用 friend.findNearest 找最近的;若是 "soldier",就先用 findByType("witch") 只取女巫,再 friend.findNearest 找最近的女巫。函数最后把选好的 target 返回出去。

# 关键片段:按兵种返回不同目标
def chooseTarget(friend):
    target = None
    if friend.type == "archer":
        # 弓箭手:离自己最近的任意敌人
        target = friend.findNearest(hero.findEnemies())
    if friend.type == "soldier":
        # 士兵:离自己最近的女巫
        witches = hero.findByType("witch")
        target = friend.findNearest(witches)
    return target

思路拆解:主循环遍历友军并下令

主循环用 for 遍历所有友军,把每个 friend 交给 chooseTarget(friend) 算出它的目标,拿到 enemy 后先判断存在再下令 hero.command(friend, "attack", enemy)。这里 if enemy: 很重要——如果场上暂时没有女巫,士兵的 target 会是 None,跳过下令即可,不会因对空目标发令而报错。

while True:
    friends = hero.findFriends()
    for friend in friends:
        enemy = chooseTarget(friend)   # 用函数决定打谁
        if enemy:
            hero.command(friend, "attack", enemy)
为什么士兵专打女巫?女巫是本关最大的威胁(溅射范围伤害)。让血厚的士兵冲上去咬住女巫,等于用最耐揍的单位去承受最危险的敌人;脆弱的弓箭手则躲在后面点掉靠近的杂兵。这就是"用对的兵打对的敌人"——针对性攻击的价值,正在于把每个单位的长处用在刀刃上。
用两个独立的 if 还是 if/elif?示例用了两个独立的 if,因为一个友军的 type 不可能同时是 archer 又是 soldier,两条永远只会命中一条,效果和 if/elif 一致。若想更严谨、少做一次判断,也可写成 if ... elif ...。两种写法在本关都正确。

3.7 常见错误

Common Mistakes

3.7.1 分支与返回值的典型坑(Pitfalls)

3.7.1.1 决策与执行的常见问题 Typical issues with return & branching

扣哒世界 分支策略常见错误速查 - koudashijie.com
图4:忘写兜底 return、混用 = 与 ==、漏判目标是否存在,是本单元最常见的失误
易错点清单:
  • 决策函数漏写兜底 return——所有条件都不满足时返回 None,主循环分流全落空,英雄无所事事。
  • 忘记用变量接住返回值——只写 chooseStrategy() 而没有 strategy = chooseStrategy(),后面就没法判断策略。
  • 把 = 当 == 用——比较相等要用 ==,= 是赋值,写错会报错或逻辑错乱。
  • 用 is 比较字符串内容——正规 Python 应用 ==;is 比较是否同一对象,判断内容相等不可靠。
  • 忘判断目标是否存在——直接 command(friend,"attack",None) 会出错,要先 if enemy:。
  • 漏了距离判断——英雄对够不着的敌人 attack 是无效动作,应先 distanceTo(...) < 25。
  • findByType 类型名拼错——类型字符串要和游戏里的准确名称一致(如 "fangrider"、"witch"),拼错会筛不到任何单位。
  • 该用 friend.findNearest 却用了 hero.findNearest——目标选择要以"友军自身"为参照,才能分散站位、各打各的。
  • 把每帧都要做的动作错放进某条分支里——像 commandAttack() 应放在 if 之外无条件执行。

本单元小结 ✨

恭喜你让程序学会了"看情况做决定"!这一单元你掌握了分支策略的完整链条:先用一个决策函数 return 出代表打法的策略字符串,再用 if / elif / else 根据返回值分流去执行不同的函数。你还学会了用 findByType 从敌群里精确筛出某一类单位、用 distanceTo 判断"够不够近才出手",并第一次写出带参数的函数 chooseTarget(friend),让士兵专打巫师、弓箭手清剿最近敌人。在"收割火焰"和"劳心劳力"两关里,你把这些工具组合起来,指挥一支分工明确的军队打出了针对性的战术。来回顾一下核心概念:

核心概念关键点对应关卡
return 返回值把结果交回调用处,遇到即结束函数收割火焰
策略字符串一个字符串代表一种打法,可读性高收割火焰
if / elif / else 分流按返回值对号入座,只走一条分支两关
== 比较相等判断内容相等用 ==,勿用 = 或 is两关
findByType 筛选只取某一类单位,配 findNearest 锁定两关
distanceTo 距离够近(如 <25)才出手,避免挥空收割火焰
带参数的函数括号里放输入口,函数体内引用它劳心劳力
针对性攻击士兵打巫师、弓箭手打最近劳心劳力

懂得先谋而后动、让每一支部队都攻其所长的英雄,才是云峰之上真正运筹帷幄的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🔥🧙

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第三单元

扣哒世界教研团队