前面几个单元里,你学会了封装函数、分支决策、用取余轮换。这一单元你要掌握一个在编程中出现频率极高的经典算法——最值算法(find the max/min):从一堆数据里挑出"最大的""最强的""最优的"那一个。在战场上,它意味着让弓箭手集火"生命值最高的大食人魔",先啃下最硬的骨头。
return 如何让函数交出一个结果、如何用变量接住返回值(archerTarget = findStrongestTarget()),亲手写出遍历数组找最大值的算法(找生命值最高的敌人),学会把返回值用于条件判断(目标是否还存活、是否需要重新寻找),并认识用三角函数把角度换算成坐标偏移,让士兵们围成一个保护圈。
findNearest 帮你找"最近的",但战术上,"最近"未必是"最该打的"。图书馆谋士这一关要求集火生命值最高的敌人——因为大食人魔血厚威胁大,先集中火力除掉它最划算。而"最高生命值"这种条件,没有现成函数可用,得你自己写算法从敌人数组里一个个比出来。这就是最值算法的用武之地。
第三单元你已经见过 return 返回字符串。这一单元的 return 更进一步:它能返回任何东西——一个数字、一个敌人单位、甚至一个 {x, y} 坐标对象。关卡注释说得很生动:"该函数返回一些东西!当你调用函数时,你会得到一些值。"findStrongestTarget() 返回的就是一个"敌人单位"对象。
函数把值 return 出来后,你要用一个变量接住它,之后才能反复使用。写法是 变量 = 函数():等号右边的函数先跑完、算出返回值,再把这个值装进等号左边的变量里。此后这个变量就"记住"了那个成果。
遍历找最大值有一个固定套路,牢牢记住这三步就能应对绝大多数"找之最"问题:
"图书馆谋士"的 findStrongestTarget 就是这套算法的标准实现。先把擂主 bestTarget 设为 None、把当前最高血量 mostHealth 设为 0;再遍历所有敌人,谁的 health 比 mostHealth 大,就让它当新擂主,并同时更新 mostHealth 为它的血量。遍历完,bestTarget 就是血量最高的那个。
def findStrongestTarget(): mostHealth = 0 # ① 擂台上还没人,纪录为 0 bestTarget = None # ① 还没有擂主 enemies = hero.findEnemies() # ② 让每个敌人上来挑战 for i in range(len(enemies)): enemy = enemies[i] if enemy.health > mostHealth: # 比擂主强? bestTarget = enemy # ③ 换新擂主 mostHealth = enemy.health # ③ 刷新纪录 # ...返回结果(见下)...
bestTarget = enemy 记住是谁,mostHealth = enemy.health 记住它多强。如果只改 bestTarget 却忘了刷新 mostHealth,比较基准就永远停在旧值,后面的比较会全部乱套,找出来的不是真正的最大值。
关卡有个战术要求:"如果有大食人魔的话,只集中弓箭手的火力。"翻译成代码就是:遍历完后,只有当擂主确实存在、且血量超过某个门槛(如 > 15)时,才把它当作值得集火的强敌返回;否则返回 None,表示"没有值得特意集火的大目标"。return 一个有意义的"空结果",是很重要的编程习惯。
# 只有存在且血量够高(是大食人魔)才值得集火 if bestTarget and bestTarget.health > 15: return bestTarget else: return None # 没有大目标,返回空
函数返回的值,除了赋给变量,还能直接拿来做条件判断。图书馆谋士的主循环里有一句关键判断:"如果 archerTarget 被击败或不存在,那就找一个新的。"它检查两件事:目标不存在(not archerTarget,比如上一次返回了 None),或者目标已阵亡(archerTarget.health <= 0)。满足任一条件,就重新调用 findStrongestTarget() 找新目标。
while True: # 目标不存在,或血量已清零 → 需要重新寻找 if not archerTarget or archerTarget.health <= 0: archerTarget = findStrongestTarget() # 接住新目标 # ...用 archerTarget 指挥弓箭手...
not 与 or 的配合:not archerTarget 在目标是 None(空)时为真;or 表示"两个条件满足任意一个即可"。合起来读作"如果没有目标,或者目标已经死了"。把两种"该换目标"的情况用 or 并在一起,逻辑清晰又周全。
这个设计很巧妙:只有当目标死了或不存在时才重新寻找,否则就一直沿用 archerTarget。这带来两个好处。其一是火力集中——所有弓箭手锁定同一个大食人魔穷追猛打,直到它倒下,而不是每帧各打各的、把伤害分散。其二是效率——不必每帧都遍历全部敌人重新计算最大值,只在真正需要时才算一次。
要让士兵们均匀地站成一圈,就得算出圈上每个点相对圆心的位置。数学上,一个点可以用"离圆心多远(半径 radius)+ 朝哪个方向(角度 angle)"来描述,这叫极坐标。而游戏里移动需要的是横坐标 x 和纵坐标 y,这叫直角坐标。两者之间的桥梁,正是三角函数 cos(余弦)和 sin(正弦)。
# 关卡已提供:把 半径+角度 换算成 x、y 偏移 def radialToCartesian(radius, degrees): radians = Math.PI / 180 * degrees # 角度转弧度 xOffset = radius * Math.cos(radians) # 横向偏移 = 半径 × cos yOffset = radius * Math.sin(radians) # 纵向偏移 = 半径 × sin return {"x": xOffset, "y": yOffset}
{x, y} 偏移量对象。这正是"把复杂细节封装进函数、只暴露简单接口"的价值——你可以站在巨人的肩膀上直接用。
要把士兵均匀铺满一圈,关键是让第 i 个士兵分到角度 i * 360 / 士兵总数。比如 4 个士兵,就分到 0°、90°、180°、270°,正好四个方向。这和第四单元"用下标把单位均分到防御点"异曲同工——都是让下标 i 均匀地对应到一圈/一组位置上。
# 关卡已提供:第 i 个士兵相对农民的偏移 def findSoldierOffset(soldiers, i): angle = i * 360 / len(soldiers) # 均分角度 return radialToCartesian(5, angle) # 半径 5 的圆上
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关目标是护送持戒农民安全撤离,不是打仗——山里潜伏着更多食人魔,恋战只会招来更多敌人。策略核心是让士兵在农民周围结成一个移动的保护圈,随农民一同前进;英雄自己也要跟紧农民。这样农民被士兵团团护住,边走边挡,安全脱身。
关卡已提供 findSoldierOffset(soldiers, i),你只要正确调用它。算法思路是:先用 findByType("soldier") 拿到士兵数组、用 findByType("peasant")[0] 拿到农民;然后 for i in range(len(soldiers)) 遍历每个士兵,用 findSoldierOffset 求出它的偏移量,把偏移量的 x、y 加到农民当前位置上,得到该士兵应站的绝对坐标,再 command(soldier, "move", moveTo) 命令它过去。
# 关键片段:偏移量 + 农民位置 = 士兵目标位置 soldiers = hero.findByType("soldier") while True: for i in range(len(soldiers)): offset = findSoldierOffset(soldiers, i) x = peasant.pos.x + offset.x # 偏移叠加到农民身上 y = peasant.pos.y + offset.y moveTo = {"x": x, "y": y} hero.command(soldiers[i], "move", moveTo) # 英雄也要跟上农民一起走
offset 是"相对农民"的固定相对位置,但 农民位置 + 偏移 得到的绝对坐标每帧都不同。放在 while 里每帧重算并重新下令,圈才能"跟着农民走",始终罩住他。
"move" 而非 "attack":本关是逃脱关,命令士兵 move 到指定位置保持队形,而不是 attack 去追敌人。追敌会打乱阵型、把农民暴露出去。牢记目标是"护送脱险",让队形优先于杀敌。
本关 Hushbaum 被伏击,她忙于治疗,指挥全交给你。战术分两层:士兵围成一圈防守,把 Hushbaum 和弓箭手护在圈内(一旦敌人能碰到她或弓箭手,就会招来更多援军);弓箭手集火最强敌人,优先啃掉血最厚的大食人魔。士兵用关卡提供的 commandSoldier 结阵,弓箭手的目标选择则要你亲手实现。
核心是补全三处。其一,写 findStrongestTarget:用前面讲的"擂主算法三步走"遍历敌人找血量最高者,并在结尾用 if bestTarget and bestTarget.health > 15 判断是否够格返回。其二,主循环里管理目标:用 if not archerTarget or archerTarget.health <= 0 判断是否需要重找,需要就 archerTarget = findStrongestTarget() 接住新目标。其三,写 commandArcher 并遍历弓箭手调用它:有强敌就打强敌,否则打最近的敌人。
# 关键片段一:找最强目标(擂主算法) def findStrongestTarget(): mostHealth = 0 bestTarget = None enemies = hero.findEnemies() for i in range(len(enemies)): enemy = enemies[i] if enemy.health > mostHealth: bestTarget = enemy mostHealth = enemy.health if bestTarget and bestTarget.health > 15: return bestTarget else: return None
# 关键片段二:主循环按需重找 + 指挥弓箭手 while True: if not archerTarget or archerTarget.health <= 0: archerTarget = findStrongestTarget() soldiers = hero.findByType("soldier") archers = hero.findByType("archer") for i in range(len(archers)): commandArcher(archers[i])
commandArcher 里的"有强敌打强敌,否则打最近":函数里先取 nearest = archer.findNearestEnemy(),再判断:若全局 archerTarget 存在就 attack 它(集火),否则退而求其次 attack 最近的敌人。这样既保证有大目标时火力集中,又保证没有大目标时弓箭手也不会闲着。
defend 结阵,把脆弱的治疗者和远程手护在中间,等于用"阵型"控制了敌人的增援节奏——这是防守反击的关键。
mostHealth = enemy.health——比较基准失效,找出的不是真最大值。mostHealth 设得太高——若设成一个很大的数,所有敌人都比不过,永远找不到擂主,应设 0。return,外面拿到的是 None,等于白找。findStrongestTarget() 而没有 archerTarget = ...,结果被丢弃。health <= 0 就一直打死目标,弓箭手会对着尸体挥箭。not archerTarget or ... 写成 and——逻辑相反,会导致该重找时不重找。offset 没加 peasant.pos,士兵会跑到地图原点附近。move 保持队形却去 attack 追敌,阵型散了、农民暴露。恭喜你掌握了编程中最经典的算法之一——最值算法!这一单元你彻底理解了 return 如何让函数交出结果、如何用 变量 = 函数() 接住返回值;亲手写出了"初始化擂台 → 逐个挑战 → 更新擂主"的遍历找最大值算法,找出生命值最高的大食人魔;学会把返回值用于条件判断(not archerTarget or health <= 0),做到目标存活就集火、阵亡才重找;还认识了用三角函数把角度换算成坐标偏移,让士兵围成保护圈。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| return 返回值 | 函数可交出任意值:数字、单位、坐标 | 两关 |
| 返回值赋给变量 | 变量 = 函数() 接住成果 | 图书馆谋士 |
| 遍历找最大值 | 初始化→逐个比较→更新擂主 | 图书馆谋士 |
| 同步更新纪录 | 换擂主时 bestTarget 与 mostHealth 一起改 | 图书馆谋士 |
| 返回值用于判断 | 用它判断目标是否存活/是否重找 | 图书馆谋士 |
| 集火与效率 | 目标活着就沿用,不必每帧重算 | 图书馆谋士 |
| 极坐标转直角 | cos/sin 把角度+半径变 x/y 偏移 | 戒指运送人 |
| 偏移叠加定位 | 农民位置 + 偏移 = 士兵目标位置 | 戒指运送人 |
懂得从千军万马中一眼锁定最强之敌、又能让部队结阵护主的英雄,才是云峰之上真正决胜千里的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️💍📚
扣哒世界 编程知识 · 计算机科学 5(安息云峰):第五单元
扣哒世界教研团队