在第一单元里你学会了用 def 把行为封装成函数,让主循环变成一份清爽的行动清单。但真实的战斗从来不是"每帧都做同样三件事"这么简单——敌人有骑士、有巫师、有弓箭手,战场形势瞬息万变。这一单元你要让程序学会看情况做决定:先分析当前局势、算出一个"该采取什么策略"的结论,再根据这个结论去执行相应的行为。这就是分支策略(branching strategy)。
return 返回一段代表策略的字符串(如 "griffin-rider"、"fight-back"),再用 if / elif / else 根据这个返回值分流去调用不同的函数;同时掌握 findByType 按类型精确筛选敌人、distanceTo 做距离判断,最后学会写带参数的函数,让士兵和弓箭手瞄准各自最合适的目标。
分支策略的核心,是把"我该怎么做"这个大问题拆成两小步:第一步决策——观察战场、算出一个结论;第二步执行——根据结论去做对应的动作。就像一位将军先派侦察兵摸清敌情("敌方骑士已经逼近雷区左侧!"),再据此下达命令("全军反击!")。程序里,"侦察摸清敌情"就是一个返回策略的函数,"据此下令"就是一串 if / elif / else。
把"决策"和"执行"分成两块,好处和第一单元"一个函数只做一件事"一脉相承:chooseStrategy 只负责思考、给出结论,具体每种策略怎么做则交给各自的函数(commandAttack、pickUpCoin、heroAttack)。将来想调整判断条件,只改决策函数;想改某种打法,只改对应的执行函数。二者互不干扰,代码清晰又好维护。
第一单元的函数都是"做完就完",不给外面任何回信。这一单元的函数不一样:它要用 return 关键字把一个值交回给调用它的地方。比如 chooseStrategy() 想清楚了就 return "fight-back",这个字符串会顺着调用点传回来,被外面的变量接住:strategy = chooseStrategy()。此后 strategy 里装的就是 "fight-back"。
"收割火焰"关卡把三种打法各起了一个字符串名字:"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" # 兜底策略
这是 return 一个非常重要、初学者常忽略的特性:只要执行到 return,函数立刻结束、把值交出去,它后面的代码一律不再运行。所以在决策函数里,把"最优先"的判断写在前面很关键——只要前面的条件满足并 return 了,后面的判断根本轮不到执行。这也让我们可以省掉很多 else:因为一旦 return,程序自然就不会走到下面。
决策函数交回策略字符串后,主循环就用 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 的缩写)是"前面都不满足时再看这个",else 是"前面全不满足时的兜底"。它们从上到下依次检查,一旦某条成立就执行对应代码块,其余分支全部跳过——保证同一时刻只做一件事。
要判断策略字符串是不是某个值,标准写法是用相等运算符 ==:if strategy == "fight-back":。注意区分:= 是"赋值"(把右边装进左边),== 才是"比较是否相等",别把两者写混。
is:扣哒世界 的部分关卡示例里用了 if strategy is "griffin-rider":。在这些教学关里它能"碰巧"工作,但在正规 Python 中,比较两个值是否相等应当用 ==;is 比较的是"是不是同一个对象",用来判断字符串内容相等并不可靠。养成好习惯:判断内容相等一律用 ==。
if strategy == "fight-back": heroAttack()
if strategy is "fight-back": heroAttack()
hero.findEnemies() 会返回所有敌人,但很多时候你只关心其中某一类——比如只想找"骑士 fangrider"、只想找"巫师 witch"。这时就用 hero.findByType("类型名"),它返回一个只含该类型单位的列表。把它和 findNearest 结合,就能精确锁定"最近的某一类单位"。
# 只找出敌方骑士,再从中挑最近的一个 fangriders = hero.findByType("fangrider") nearestRider = hero.findNearest(fangriders) # 也可以一行写完:找出所有巫师里最近的那个 nearestWitch = hero.findNearest(hero.findByType("witch"))
筛出来的是一个列表,可以用 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" # 立即决定反击
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 不是空(确实找到了敌人),右边再算距离。若敌人根本不存在,左边为假就直接跳过,不会因为对"空目标"求距离而出错。这种"先确认存在、再进一步判断"的写法很常用。
前面的函数括号都是空的,这一节的函数括号里要放一个参数。参数就是函数的"输入口":调用时你塞进去一个值,函数体内就能用这个名字来引用它。"劳心劳力"要求定义 chooseTarget(friend)——它接收一个 friend(某个友军单位),根据这个 friend 的兵种,返回它该攻击的目标。
# friend 是参数:调用时传进来一个友军单位 def chooseTarget(friend): target = None # 函数体里可以直接用 friend 这个名字 if friend.type == "archer": # ... pass return target
"劳心劳力"的战术精髓,是让不同兵种各打各的目标:士兵(soldier)血厚、能扛,让它们冲上去缠住危险的女巫(witch);弓箭手(archer)脆但远程,让它们优先清掉离自己最近的任意敌人。判断依据就是 friend.type——用它区分这个友军是士兵还是弓箭手,再返回相应的目标。
if friend.type == "archer": enemies = hero.findEnemies() target = friend.findNearest(enemies)
if friend.type == "soldier": witches = hero.findByType("witch") target = friend.findNearest(witches)
friend.findNearest(witches),而不是 hero.findNearest。因为我们要的是离这名友军自己最近的目标,而不是离英雄最近的。这样每个士兵会各自扑向离它最近的女巫,把士兵自然地分成几组,避免全挤在一处被女巫的溅射(范围伤害)一锅端。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关目标是生存 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()
if 之前无条件执行,保证格里芬骑士时刻都在战斗;而召唤、反击、捡币这三件事才是"看情况三选一"的分支。分清"总要做的"和"看情况做的",是组织主循环的关键。
return "collect-coins",函数会返回 None,主循环的 if/elif 一条都匹配不上,英雄这一帧就什么都不做、白白浪费时间。永远给决策函数一个兜底返回值。
本关登场的女巫(witch)攻击带范围溅射,友军挤成一团很容易被一锅端。破局思路是让不同兵种分工瞄准不同目标:士兵(soldier)冲上去各自缠住离自己最近的女巫,把队伍自然拆成几个小组分散站位;弓箭手(archer)则远程清掉离自己最近的任意敌人。这样既压制了危险的女巫,又避免了扎堆挨溅射。
核心是写一个带参数的 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,因为一个友军的 type 不可能同时是 archer 又是 soldier,两条永远只会命中一条,效果和 if/elif 一致。若想更严谨、少做一次判断,也可写成 if ... elif ...。两种写法在本关都正确。
return——所有条件都不满足时返回 None,主循环分流全落空,英雄无所事事。chooseStrategy() 而没有 strategy = chooseStrategy(),后面就没法判断策略。= 当 == 用——比较相等要用 ==,= 是赋值,写错会报错或逻辑错乱。is 比较字符串内容——正规 Python 应用 ==;is 比较是否同一对象,判断内容相等不可靠。command(friend,"attack",None) 会出错,要先 if enemy:。attack 是无效动作,应先 distanceTo(...) < 25。"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(安息云峰):第三单元
扣哒世界教研团队