欢迎来到计算机科学 6!你已经能熟练使用函数、数组、循环和条件判断了。在这个进阶篇章的第一单元,你将迎来一个全新的挑战:不再只操纵英雄一个人,而是同时指挥一支由农民、圣骑士、狮鹫骑士组成的队伍。要让这么多单位各司其职、不互相"抢活"、还能在恰当时机施法治疗——这需要更高级的编程思维。本单元的核心,是学会用最优目标决策(optimal target selection)和协作过滤来指挥一支高效协同的队伍。
commandPaladin、findBestItem);用 in 检查过滤掉"已被别人选中"的目标,避免重复;用价值/距离比值挑出性价比最高的金币;用 canCast 判断技能是否可用、用 command 指挥单位施放治疗或盾牌、用 buildXY 建造诱饵;并写出"寻找生命值最低友军"的求最小值算法。
指挥一支队伍的第一步,是按兵种分工。你会遍历所有友军,判断每个单位的 type,再根据类型调用对应的指挥函数——农民去收集金币、圣骑士负责治疗与防御、狮鹫骑士负责进攻。这种"分类调度"结构让复杂的团队行为变得井井有条。
# 遍历所有友军,按兵种分派任务 def commandFriends(): friends = hero.findFriends() for friend in friends: if friend.type == "peasant": commandPeasant(friend) # 农民收集金币 elif friend.type == "griffin-rider": commandGriffin(friend) # 狮鹫进攻 elif friend.type == "paladin": commandPaladin(friend) # 圣骑士治疗/防御
回顾计算机科学 5 学的函数思想——"一个函数做一件事"。在团队指挥里,这个原则升级为"一个兵种一个函数"。把圣骑士该做的所有判断(治疗?防御?攻击?)都封装进 commandPaladin,主循环就只需一句调用。这样代码清晰、易于调试,某个兵种的行为出问题时也能快速定位。
commandPaladin 负责所有圣骑士的决策,commandGriffin 负责所有狮鹫。总指挥只管"点名派活",具体怎么打交给小队长——这就是封装的力量。
当多个农民同时找金币时,一个大问题浮现:如果他们都盯上同一枚金币,就会挤在一起抢、效率极低。解决办法是维护一个"已被认领"的清单,每个农民选目标前,先用 in 运算符检查这枚金币是不是已经被别人选走了。如果在清单里,就用 continue 跳过它。
for item in items: # 用 in 检查这枚金币是否已被别的农民认领 if item in excludedItems: continue # 已被认领,跳过它 # ...否则考虑把它作为候选目标...
x in 数组 会返回 True 或 False,表示 x 是否是数组里的一个元素。它省去了手写循环逐个比对的麻烦,是判断"某物是否已存在于集合中"的利器。
这个模式分三步:每轮循环开始时创建一个空的已选清单;每个农民选定目标后,把该目标 append 进清单;后续农民选目标时用 in 跳过清单里的项。这样就实现了"分工不撞车"——每枚金币最多被一个农民认领。
while True: peasants = hero.findByType("peasant") claimedItems = [] # 每轮循环都新建一个空清单 for peasant in peasants: item = findBestItem(peasant, claimedItems) if item: claimedItems.append(item) # 认领:加入清单 hero.command(peasant, "move", item.pos)
claimedItems = [] 必须放在 while 循环内部、每一帧都重置。因为金币的分配是逐帧重新计算的,如果清单放在循环外只建一次,旧的认领记录会残留,导致农民无金币可选。
怎样算"最优"的金币?只看价值不行——一枚很值钱但在天边的金币,跑过去的时间够捡好几枚近的了;只看距离也不行——最近的可能只值一点点。真正的性价比是价值除以距离:item.value / friend.distanceTo(item)。这个比值越大,说明"单位路程能换来的价值"越高,就越值得去。
要在一堆金币里找出比值最高的那个,用经典的"打擂台"求最大值算法:先设一个"擂主" bestItem = None 和它的比值 bestItemValue = 0,遍历每枚(未被认领的)金币,算出它的比值,若比当前擂主更高,就让它当新擂主。遍历结束,擂台上的就是最优目标。这正是关卡 findBestItem 函数的核心。
def findBestItem(friend, excludedItems): items = friend.findItems() bestItem = None bestItemValue = 0 # 擂主初始比值 for item in items: if item in excludedItems: # 过滤已认领 continue value = item.value / friend.distanceTo(item) if value > bestItemValue: # 打赢擂主 bestItem = item bestItemValue = value return bestItem
return bestItem 交回调用处。如果找不到合适目标(比如全被认领了),bestItem 会保持 None,调用方需要判断"如果有目标才行动"。
圣骑士的治疗术不是随时能用的——它有冷却时间。贸然施法会失败、浪费一帧。所以施法前必须用 canCast("heal", target) 检查:它返回 True 表示"这个技能现在可以对这个目标施放",返回 False 则说明还在冷却或目标不合适。只有确认可用,才下令施法。
# 施法前先用 canCast 确认技能可用 if lowest and paladin.canCast("heal", lowest): hero.command(paladin, "cast", "heal", lowest)
hero.command(单位, 动作, ...) 是指挥友军的总接口。第一个参数是要指挥的单位,第二个是动作名,后面跟该动作需要的额外参数。同一个 command 能让单位移动、攻击、施法、防御——通过不同的动作名切换。圣骑士的三种典型指挥如下:
| 指挥写法 | 作用 |
|---|---|
| command(p, "cast", "heal", target) | 让圣骑士 p 对 target 施放治疗 |
| command(p, "shield") | 让圣骑士 p 举盾防御,承受更少伤害 |
| command(p, "attack", enemy) | 让圣骑士 p 用狼牙棒攻击 enemy |
if / elif / elif 把这三种情况按优先级串起来,就能让圣骑士"该救人时救人、该保命时保命、没事时打人"。
当农民成为食人魔的攻击目标时,直接逃跑往往来不及。更聪明的做法是命令农民建造一个诱饵(decoy)——一个假目标,把食人魔吸引过去(在关卡里还能顺势把敌人引入箭塔的射程)。建造用 command(peasant, "buildXY", "decoy", x, y),关卡建议在 peasant.pos.x - 2 处建造。
# 农民被敌人盯上、且金币够(诱饵需要 25 金)时建诱饵 if enemy.target == peasant and enoughGoldForDecoy(): hero.command(peasant, "buildXY", "decoy", peasant.pos.x - 2, peasant.pos.y) continue # 建造时就别再去收集金币了
continue 跳过本轮剩下的"收集金币"逻辑,否则会覆盖掉建造命令,诱饵就建不成了。
治疗应该优先给伤得最重的人。找"生命值最低的友军"用的是求最大值的镜像——求最小值算法:先设一个大得离谱的初始值 lowestHealth = 99999,遍历每个友军,谁的血量比当前记录更低,就更新记录。这里还加了两个过滤条件:只看圣骑士(type != "paladin" 就跳过),且只考虑真正受伤的(health < maxHealth)。
def lowestHealthPaladin(): lowestHealth = 99999 # 故意设很大,方便被刷新 lowestFriend = None friends = hero.findFriends() for friend in friends: if friend.type != "paladin": continue # 只关心圣骑士 if friend.health < lowestHealth and friend.health < friend.maxHealth: lowestHealth = friend.health lowestFriend = friend return lowestFriend
lowestHealth 必须比任何可能的血量都大(如 99999),这样第一个受伤友军就一定能"打赢"它、成功刷新记录。反过来,求最大值时初始值要设得很小(如 0)。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关要指挥一群农民高效收集金币。两大挑战:其一,别让农民抢同一枚金币——要用"已选清单"分工;其二,农民会被食人魔追杀——被盯上时建诱饵把敌人引入箭塔。策略是:每帧遍历所有农民,被追的先建诱饵自保,其余的各自挑一枚性价比最高、且没被同伴认领的金币去捡。
算法有三块。第一,补全 findBestItem:用价值/距离比值打擂台,同时用 in 过滤 excludedItems 里已认领的金币,返回最优目标。第二,遇袭建诱饵:若 enemy.target == peasant(该农民正是敌人的目标)且 enoughGoldForDecoy()(金币≥25),就 buildXY 建诱饵并 continue。第三,认领并收币:为农民选出最优金币后,先 append 进 claimedItems 再命令它去捡。
# 关键片段:诱饵优先,否则认领并收集最优金币 for peasant in peasants: enemy = peasant.findNearestEnemy() if enemy and enemy.target == peasant and enoughGoldForDecoy(): hero.command(peasant, "buildXY", "decoy", peasant.pos.x - 2, peasant.pos.y) continue # 建造时不再收集金币 item = findBestItem(peasant, claimedItems) if item: claimedItems.append(item) hero.command(peasant, "move", item.pos)
target(当前锁定的目标)。判断 enemy.target == peasant 就是问"这个食人魔正在追的是不是这个农民"。只有正被追杀的农民才需要建诱饵自救,其他农民照常收币,避免浪费金币。
enoughGoldForDecoy() 确认金币够。没钱硬建会失败。这也是"行动前先检查前提条件"这一防御性编程思想的体现。
本关要顶住食人魔巫师的攻势、保护圣骑士 Reynaldo。你要指挥三个兵种:圣骑士负责治疗与防御、农民收集金币、狮鹫骑士负责进攻,同时在金币充足时不断召唤新的狮鹫增援。策略核心是给每个兵种写好独立的指挥函数,再在主循环里统一调度并召唤。
最关键的是 commandPaladin,它要按优先级做三选一。第一优先治疗:用 lowestHealthPaladin() 找到最伤的圣骑士,若存在且 canCast("heal", lowest) 就施治疗。第二优先防御:若自己血量低于最大值的某个比例(如 70%),就 shield 举盾。第三攻击:否则找最近的敌人用狼牙棒 attack。三种情况用 if / elif / elif 串联,保证"救人 > 保命 > 输出"。
# 关键片段:圣骑士按优先级治疗 / 防御 / 攻击 def commandPaladin(paladin): nearest = paladin.findNearestEnemy() lowest = lowestHealthPaladin() if lowest and paladin.canCast("heal", lowest): hero.command(paladin, "cast", "heal", lowest) # 先救人 elif paladin.health < paladin.maxHealth * 0.7: hero.command(paladin, "shield") # 再保命 elif nearest: hero.command(paladin, "attack", nearest) # 最后输出
主循环每帧调用 commandFriends() 统一指挥所有兵种;之后判断金币是否够召唤狮鹫(hero.gold >= hero.costOf("griffin-rider")),够就 hero.summon("griffin-rider")。狮鹫的指挥 commandGriffin 则很直接——找到最近敌人就攻击,没敌人就移动到指定集结点待命。
# 关键片段:主循环统一调度 + 有钱就召唤狮鹫 while True: commandFriends() if hero.gold >= hero.costOf("griffin-rider"): hero.summon("griffin-rider")
if / elif 结构天然实现了优先级——一旦满足前面的条件就不会执行后面的,所以把最重要的动作(治疗)放在最前面至关重要。
lowestHealthPaladin 里的 health < maxHealth 条件很重要——它保证只把满血的圣骑士排除在外。否则函数可能返回一个满血单位,白白浪费一次治疗的冷却。
claimedItems = [] 必须在 while 循环内每帧重建,放循环外会导致旧认领残留、农民无币可选。append 进清单,否则 in 过滤形同虚设,农民仍会撞车。cast 而不检查冷却,会施法失败、浪费回合。continue 会让后面的收币命令覆盖建造。findBestItem、lowestHealthPaladin 必须 return 结果,否则调用方拿到 None。health < maxHealth 条件,避免对满血单位浪费治疗。恭喜你从"单人英雄"晋级为"团队指挥官"!这一单元你学会了用函数把每个兵种的行为封装成独立的指挥逻辑;用 in 检查配合"已选清单"过滤掉被认领的目标,让农民分工不撞车;用价值/距离比值打擂台挑出性价比最高的金币;用 canCast 在施法前检查技能可用、用 command 指挥单位治疗(cast heal)、防御(shield)或攻击、用 buildXY 建造诱饵引开敌人;还掌握了"寻找生命值最低友军"的求最小值算法。在"雾岛矿"和"严峻的决心"两关里,你把这些技巧融会贯通,指挥出一支高效协同的队伍。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 函数封装协作 | 一个兵种一个指挥函数 | 两关 |
| in 检查过滤 | if item in excludedItems | 雾岛矿 |
| 已选清单 | 认领后 append,循环内重建 | 雾岛矿 |
| 价值/距离比值 | value / distanceTo 打擂台 | 雾岛矿 |
| buildXY 建诱饵 | 引开敌人,建完 continue | 雾岛矿 |
| canCast 判可用 | 施法前检查冷却 | 严峻的决心 |
| command 指挥 | cast / shield / attack | 严峻的决心 |
| 求最低血量 | 初值设大,遍历刷新最小 | 严峻的决心 |
懂得让每个单位各司其职、用数据权衡出最优选择、在危急时刻做对决策的英雄,才是云峰之上真正运筹帷幄的指挥官。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🎖️⚔️
扣哒世界 编程知识 · 计算机科学 6(安息云峰):第一单元
扣哒世界教研团队