去做题

第一单元:最优目标 · 协作指挥与价值决策

Unit 1: Optimal Targets — Coordinated Command & Value-Based Decisions
计算机科学 6 · 安息云峰 Cloudrip Mountain · 雾岛矿 → 严峻的决心

1.1 协作指挥概述

Overview of Coordinated Command

欢迎来到计算机科学 6!你已经能熟练使用函数、数组、循环和条件判断了。在这个进阶篇章的第一单元,你将迎来一个全新的挑战:不再只操纵英雄一个人,而是同时指挥一支由农民、圣骑士、狮鹫骑士组成的队伍。要让这么多单位各司其职、不互相"抢活"、还能在恰当时机施法治疗——这需要更高级的编程思维。本单元的核心,是学会用最优目标决策(optimal target selection)和协作过滤来指挥一支高效协同的队伍。

本单元主线:把每类单位的行为封装成独立函数(如 commandPaladin、findBestItem);用 in 检查过滤掉"已被别人选中"的目标,避免重复;用价值/距离比值挑出性价比最高的金币;用 canCast 判断技能是否可用、用 command 指挥单位施放治疗或盾牌、用 buildXY 建造诱饵;并写出"寻找生命值最低友军"的求最小值算法。

1.1.1 从个人到团队(From One Hero to a Team)

1.1.1.1 遍历不同兵种,分类指挥 Iterate over unit types and dispatch by role

指挥一支队伍的第一步,是按兵种分工。你会遍历所有友军,判断每个单位的 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)       # 圣骑士治疗/防御

1.1.2 用函数封装协作逻辑(Encapsulating Logic in Functions)

1.1.2.1 一个兵种,一个函数 One unit type, one function

回顾计算机科学 5 学的函数思想——"一个函数做一件事"。在团队指挥里,这个原则升级为"一个兵种一个函数"。把圣骑士该做的所有判断(治疗?防御?攻击?)都封装进 commandPaladin,主循环就只需一句调用。这样代码清晰、易于调试,某个兵种的行为出问题时也能快速定位。

🎖️ 像军队的指挥链
总指挥(主循环)不必亲自告诉每个士兵每一步怎么做,而是把命令下达给各兵种的"小队长"函数:commandPaladin 负责所有圣骑士的决策,commandGriffin 负责所有狮鹫。总指挥只管"点名派活",具体怎么打交给小队长——这就是封装的力量。

1.2 避免重复选择

Avoiding Duplicate Choices

1.2.1 in 检查过滤(Filtering with in)

1.2.1.1 用 in 判断元素是否已在数组里 Use in to test membership

当多个农民同时找金币时,一个大问题浮现:如果他们都盯上同一枚金币,就会挤在一起抢、效率极低。解决办法是维护一个"已被认领"的清单,每个农民选目标前,先用 in 运算符检查这枚金币是不是已经被别人选走了。如果在清单里,就用 continue 跳过它。

for item in items:
    # 用 in 检查这枚金币是否已被别的农民认领
    if item in excludedItems:
        continue   # 已被认领,跳过它
    # ...否则考虑把它作为候选目标...
in 运算符:x in 数组 会返回 True 或 False,表示 x 是否是数组里的一个元素。它省去了手写循环逐个比对的麻烦,是判断"某物是否已存在于集合中"的利器。

1.2.2 已选清单模式(The Claimed-List Pattern)

1.2.2.1 认领后加入清单,下一个就绕开 Claim it, then add to the list

这个模式分三步:每轮循环开始时创建一个空的已选清单;每个农民选定目标后,把该目标 append 进清单;后续农民选目标时用 in 跳过清单里的项。这样就实现了"分工不撞车"——每枚金币最多被一个农民认领。

已选清单:认领一个就把它排除掉 农民 A 选金币💰1 claimedItems [💰1] 农民 B 跳过💰1,选💰2 每枚金币只被一个农民认领,分工不撞车
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 循环内部、每一帧都重置。因为金币的分配是逐帧重新计算的,如果清单放在循环外只建一次,旧的认领记录会残留,导致农民无金币可选。

1.3 最优目标决策

Choosing the Optimal Target

1.3.1 价值/距离比值(Value-to-Distance Ratio)

1.3.1.1 又值钱又近的才是好目标 The best target is both valuable and close

怎样算"最优"的金币?只看价值不行——一枚很值钱但在天边的金币,跑过去的时间够捡好几枚近的了;只看距离也不行——最近的可能只值一点点。真正的性价比是价值除以距离:item.value / friend.distanceTo(item)。这个比值越大,说明"单位路程能换来的价值"越高,就越值得去。

性价比 = 价值 ÷ 距离 item.value / distanceTo(item) 金币值多少 要跑多远 → 越大越好
🪙 举个例子
金币 A 值 5,距离 10 → 比值 0.5;金币 B 值 3,距离 3 → 比值 1.0。虽然 A 更值钱,但 B 的性价比更高(每单位距离换来更多价值),所以应该优先去捡 B。这就是贪心策略里的"局部最优选择"。

1.3.2 打擂台求最优(Finding the Max by Comparison)

1.3.2.1 遍历所有候选,留下比值最高的 Iterate and keep the highest ratio

要在一堆金币里找出比值最高的那个,用经典的"打擂台"求最大值算法:先设一个"擂主" 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:决策函数一定要把选出的最优目标 return bestItem 交回调用处。如果找不到合适目标(比如全被认领了),bestItem 会保持 None,调用方需要判断"如果有目标才行动"。

1.4 技能与指挥

Abilities and Commands

1.4.1 canCast 判断技能是否可用(Checking with canCast)

1.4.1.1 施法前先问"冷却好了吗" Ask if the spell is ready before casting

圣骑士的治疗术不是随时能用的——它有冷却时间。贸然施法会失败、浪费一帧。所以施法前必须用 canCast("heal", target) 检查:它返回 True 表示"这个技能现在可以对这个目标施放",返回 False 则说明还在冷却或目标不合适。只有确认可用,才下令施法。

# 施法前先用 canCast 确认技能可用
if lowest and paladin.canCast("heal", lowest):
    hero.command(paladin, "cast", "heal", lowest)

1.4.2 command 指挥单位(Commanding Units)

1.4.2.1 一条命令,多种动作 One command, many actions

hero.command(单位, 动作, ...) 是指挥友军的总接口。第一个参数是要指挥的单位,第二个是动作名,后面跟该动作需要的额外参数。同一个 command 能让单位移动、攻击、施法、防御——通过不同的动作名切换。圣骑士的三种典型指挥如下:

指挥写法作用
command(p, "cast", "heal", target)让圣骑士 p 对 target 施放治疗
command(p, "shield")让圣骑士 p 举盾防御,承受更少伤害
command(p, "attack", enemy)让圣骑士 p 用狼牙棒攻击 enemy
优先级顺序很关键:圣骑士的决策应有明确优先级——先看有没有受伤的队友要治,再看自己血量低不低(低就举盾),最后才去攻击。用 if / elif / elif 把这三种情况按优先级串起来,就能让圣骑士"该救人时救人、该保命时保命、没事时打人"。

1.4.3 buildXY 建造诱饵(Building a Decoy)

1.4.3.1 用诱饵把敌人引开 Distract enemies with a decoy

当农民成为食人魔的攻击目标时,直接逃跑往往来不及。更聪明的做法是命令农民建造一个诱饵(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:农民一帧只能做一件事。命令它建诱饵后,要用 continue 跳过本轮剩下的"收集金币"逻辑,否则会覆盖掉建造命令,诱饵就建不成了。

1.5 寻找最低血量友军

Finding the Lowest-Health Ally

1.5.1 求最小值算法(The Minimum-Finding Algorithm)

1.5.1.1 打"反向擂台"找最伤者 Reverse the tournament to find the minimum

治疗应该优先给伤得最重的人。找"生命值最低的友军"用的是求最大值的镜像——求最小值算法:先设一个大得离谱的初始值 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
遍历比较,留下血量最低的友军 HP 80健康 HP 45受伤 HP 12 ★最危险! HP 90健康 优先治疗血量最低的这一位
初始值为什么设很大:求最小值时,初始的 lowestHealth 必须比任何可能的血量都大(如 99999),这样第一个受伤友军就一定能"打赢"它、成功刷新记录。反过来,求最大值时初始值要设得很小(如 0)。

1.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 雾岛矿

⛏️ 雾岛矿 —— 农民协作收币与建诱饵

扣哒世界 雾岛矿关卡 - koudashijie.com
图2:雾岛矿——多个农民分工收集金币,各选性价比最高且未被认领的目标,被追时建诱饵把食人魔引入箭塔

策略:分工收币,遇袭建诱饵

本关要指挥一群农民高效收集金币。两大挑战:其一,别让农民抢同一枚金币——要用"已选清单"分工;其二,农民会被食人魔追杀——被盯上时建诱饵把敌人引入箭塔。策略是:每帧遍历所有农民,被追的先建诱饵自保,其余的各自挑一枚性价比最高、且没被同伴认领的金币去捡。

思路拆解

算法有三块。第一,补全 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)
enemy.target == peasant 的含义:每个敌人都有一个 target(当前锁定的目标)。判断 enemy.target == peasant 就是问"这个食人魔正在追的是不是这个农民"。只有正被追杀的农民才需要建诱饵自救,其他农民照常收币,避免浪费金币。
诱饵成本要检查:建诱饵要花 25 金,所以务必先用 enoughGoldForDecoy() 确认金币够。没钱硬建会失败。这也是"行动前先检查前提条件"这一防御性编程思想的体现。
关卡 · 严峻的决心

🛡️ 严峻的决心 —— 圣骑士治疗、防御与召唤

扣哒世界 严峻的决心关卡 - koudashijie.com
图3:严峻的决心——圣骑士按优先级治疗最伤的队友、危急时举盾、平时攻击,配合农民收币、召唤狮鹫作战

策略:多兵种协同,保护 Reynaldo

本关要顶住食人魔巫师的攻势、保护圣骑士 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 条件很重要——它保证只把满血的圣骑士排除在外。否则函数可能返回一个满血单位,白白浪费一次治疗的冷却。

1.7 常见错误

Common Mistakes

1.7.1 新手最容易踩的坑(Pitfalls for Beginners)

1.7.1.1 协作指挥的典型问题 Typical issues with team command

扣哒世界 协作指挥常见错误速查 - koudashijie.com
图4:清单位置错、忘记认领、优先级颠倒、施法前不检查,是本单元最常见的失误
易错点清单:
  • 已选清单放错位置——claimedItems = [] 必须在 while 循环内每帧重建,放循环外会导致旧认领残留、农民无币可选。
  • 选中目标却忘记认领——选完金币要 append 进清单,否则 in 过滤形同虚设,农民仍会撞车。
  • 圣骑士优先级颠倒——治疗要在攻击之前判断,否则圣骑士只顾打架、队友阵亡。
  • 施法前不用 canCast——直接 cast 而不检查冷却,会施法失败、浪费回合。
  • 建诱饵后忘写 continue——农民一帧只能做一件事,不 continue 会让后面的收币命令覆盖建造。
  • 求最值初始值设反——求最小血量初值要很大(99999),求最大比值初值要很小(0),设反了永远刷不出结果。
  • 决策函数忘写 return——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(安息云峰):第一单元

扣哒世界教研团队