欢迎来到封神冰川的最后一个单元!在这片被冰雪封印的战场上,你的指挥总部下达了一套加密的部署命令——所有指令被压缩进了一根指令牌(sign)的一长串文本里。你不能再靠肉眼一个个数坐标,而要让程序自动读懂这串文本、召唤正确的兵种、并把它们精确送到指定坐标。这正是字符串算法(string algorithms)大显身手的地方:把一段看似杂乱的字符,按照约定的规则拆解成有意义的信息。
substr(字符串截取)按索引取出子串;用字典(dict)把类型字母映射成兵种名;识别 Txxyy 这种固定格式命令;用步长为 5 的循环逐条解析整条消息;最后用 hero.summon 召唤单位、hero.command 把单位移动到解析出的坐标。
字符串就是一串按顺序排列的字符——字母、数字、符号都算。它和数组很像:每个字符都有一个索引(index),从 0 开始编号。比如字符串 "a1536" 里,第 0 个字符是 a,第 1 个是 1,第 2 个是 5,以此类推。正是因为字符有固定的位置,我们才能"按位置"把想要的部分精准截取出来。
关卡里的部署命令写在场上的一块指令牌(sign)上。先用 hero.findByType("sign") 找到它,取出第一块(索引 [0]),再读取它的 .message 属性——这就是我们要解码的那一长串文本。
# 找到指令牌,读取上面的部署命令 sign = hero.findByType("sign")[0] message = sign.message # message 现在是类似 "a1536s4532P2455..." 的一长串字符
message 拆成一条条 Txxyy 命令,再从每条里分离出兵种类型和坐标。学会解析,是处理真实世界数据(配置文件、网络协议、日志)的基本功。
字符串截取用 substr(start, length):从索引 start 处开始,取 length 个字符,返回一个新的子字符串。它不改变原字符串,只是"复制出"其中一段。这是本单元最核心的工具——正是靠它,我们才能把 Txxyy 拆成 T、xx、yy 三部分。
message[i] 只取一个字符;而 message.substr(i, n) 能取连续 n 个字符。坐标 xx 是两位数,必须用 substr(i, 2) 一次取两个字符,才能得到完整的 "15" 而不是单个 "1"。
假设某条命令从索引 index 开始。类型字母 T 在第 index 位,取 1 个;x 坐标在第 index+1 位,取 2 个;y 坐标在第 index+3 位,取 2 个。三次 substr 就把一条命令拆得清清楚楚。
# 从 index 位置拆出一条 Txxyy 命令 type = message.substr(index, 1) # 类型字母,如 "a" x = message.substr(index + 1, 2) # x 坐标,如 "15" y = message.substr(index + 3, 2) # y 坐标,如 "36"
substr 取出的 "15" 是文本而不是数字。要用它来定位坐标,后面必须用 int() 把它转成整数(详见 6.5.3),否则 "15"+"36" 会变成拼接成 "1536",而不是我们要的坐标。
命令里的类型是单个字母(a、s、p…),但 hero.summon 需要的是完整的兵种名("archer"、"soldier"…)。如果用一长串 if / elif 一一对应会很啰嗦。更优雅的做法是用字典(dictionary)——一张"键值对照表",把每个字母(键)映射到对应的兵种名(值)。
# 字典:字母 → 兵种名 的对照表 summonTypes = { "a": "archer", # 弓箭手 "s": "soldier", # 士兵 "p": "peasant", # 农民 "g": "griffin-rider", # 狮鹫骑士 "P": "paladin", # 圣骑士 "A": "artillery" # 炮兵 }
| 命令字母(键) | 兵种名(值) | 说明 |
|---|---|---|
| "a" | "archer" | 弓箭手,远程输出 |
| "s" | "soldier" | 士兵,近战肉盾 |
| "p" | "peasant" | 农民,收集资源 |
| "g" | "griffin-rider" | 狮鹫骑士,飞行进攻 |
| "P" | "paladin" | 圣骑士,治疗防御 |
| "A" | "artillery" | 炮兵,范围轰炸 |
"p"(小写,农民 peasant)和 "P"(大写,圣骑士 paladin)是两个不同的键!字典和字符串都严格区分大小写,抄写命令表时千万别把它们混为一谈。
建好字典后,用 字典[键] 的写法就能瞬间查到对应的值。比如解析出类型字母是 "a",那么 summonTypes["a"] 立刻返回 "archer"。这比一长串 if 判断简洁得多,而且新增兵种时只要往字典里加一行即可。
# 用解析出的字母作键,查出完整兵种名 letter = "a" unitType = summonTypes[letter] # 得到 "archer" hero.summon(unitType)
整条消息由若干条命令首尾相接拼成,每条命令都严格遵守 Txxyy 这个固定格式(fixed format):1 个类型字母 + 2 位 x 坐标 + 2 位 y 坐标,正好 5 个字符。正因为长度固定,我们才能靠"数格子"精确定位每条命令的起点——第 1 条从 0 开始,第 2 条从 5 开始,第 3 条从 10 开始……
要每次跳到下一条命令的起点,就得让循环变量每轮增加 5 而不是 1。range 的第三个参数正是步长(step):range(0, len(message), 5) 会依次产生 0, 5, 10, 15, ...——恰好是每条命令的起始索引。这样循环体每跑一次,正好处理一条完整命令。
for i in range(0, 15): # 0,1,2,3,...,14 # 每次只前进 1 个字符
for i in range(0, 15, 5): # 0,5,10 # 每次跳过一整条命令
# 以步长 5 遍历整条消息,逐条解析每个 Txxyy for index in range(0, len(message), 5): type = message.substr(index, 1) x = message.substr(index + 1, 2) y = message.substr(index + 3, 2) summonTroops(type, x, y) # 交给召唤函数处理
len(message) 作为循环终点,无论消息多长都能自动跑完——这让代码对任意长度的命令串都通用。因为每条命令都是 5 个字符且首尾相接,len(message) 一定是 5 的整数倍。
hero.summon("archer") 会消耗一定金币,在你身边召唤出一个弓箭手。召唤需要 Boss Star 权限——不同兵种要求不同等级的 Boss Star(士兵最低,圣骑士、炮兵较高)。召唤前应先用 hero.costOf(type) 查询价格,确认 hero.gold 够用,避免召唤失败。
# 金币足够时才召唤,避免浪费一帧 if hero.gold > hero.costOf(type): hero.summon(type)
召唤只是把单位变出来,它还停在你身边。要把它送到指定坐标,得用 hero.command(单位, "move", 目标位置)。难点在于:怎么在一群同类单位里找到刚刚召唤的那一个?技巧是——召唤后重新查一次该兵种的所有单位,数组里的最后一个(索引 len(friends)-1)就是最新召唤的。
# 召唤后,取该兵种数组的最后一个 = 刚召唤的单位 friends = hero.findByType(unitType, hero.findFriends()) newUnit = friends[len(friends) - 1] hero.command(newUnit, "move", Vector(int(x), int(y)))
friends[len(friends)-1](即最后一个元素)总是最新那一个。指挥它移动,就不会误派到之前已经就位的同类单位。
substr 得到的 x、y 还是文本(如 "15"、"36")。移动指令需要的是数字坐标,所以要用 int() 把它们转成整数,再用 Vector(int(x), int(y)) 打包成一个位置向量传给 command。
"15" 当坐标,程序无法正确计算位置;而且用 + 连接两个字符串是拼接不是相加("1"+"5" 得到 "15" 而非 6)。所以取出的坐标一定要 int() 转成数字后再使用。
这一节聚焦关卡的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关的部署命令全部编码在一块指令牌里,形如 "a1536s4532P2455..."。你的任务是把这串"密文"翻译成实际行动。整体策略分四步:读牌——从 sign.message 取出命令串;解析——以步长 5 逐条拆出类型和坐标;召唤——用字典把字母翻译成兵种名再 summon;部署——把刚召唤的单位 command 移动到解析出的坐标。
解析出来的类型是字母,但召唤需要兵种全名。与其写六个 if / elif,不如建一张字典 summonTypes,把 "a"→"archer"、"s"→"soldier" …一次性对应好。这样"翻译"只需一句 summonTypes[num],代码短、也更易扩展。这体现了用数据结构消除重复分支的思想。
# 关键片段:字母→兵种 的字典映射 summonTypes = { "a": "archer", "s": "soldier", "p": "peasant", "g": "griffin-rider", "P": "paladin", "A": "artillery" }
把"召唤某兵种并送到 (x, y)"这套动作封装进一个函数 summonTroops(num, coorX, coorY),主循环只管解析、然后调用它。函数内部:先用字典查出兵种名,检查金币是否够,summon 召唤,再取该兵种数组的最后一个(最新召唤的),command 它移动到 Vector(int(coorX), int(coorY))。注意炮兵(artillery)等高级兵种可能因权限或金币而单独判断。
# 关键片段:召唤并部署一个单位(封装成函数) def summonTroops(num, coorX, coorY): unitType = summonTypes[num] # 字典翻译字母 if hero.gold > hero.costOf(unitType): hero.summon(unitType) friends = hero.findByType(unitType, hero.findFriends()) newUnit = friends[len(friends) - 1] # 最新召唤的 hero.command(newUnit, "move", Vector(int(coorX), int(coorY)))
最后,用步长为 5 的循环遍历整条消息:每轮从 index 处用三次 substr 拆出类型、x、y,交给 summonTroops。循环跑完,全部命令就依次执行完毕。这就是解析固定格式数据的经典骨架。
# 关键片段:以步长 5 逐条解析并执行命令 sign = hero.findByType("sign")[0] message = sign.message for index in range(0, len(message), 5): type = message.substr(index, 1) x = message.substr(index + 1, 2) y = message.substr(index + 3, 2) summonTroops(type, x, y)
hero.findByType("sign")[8],但通常应取索引 [0] 才是那块部署指令牌。若拿错指令牌,读到的 message 就不对。开始前不妨用 hero.say(message) 把消息打印出来核对格式。
while True 里而不加控制,会不断重复召唤同一批单位、瞬间耗光金币。本关的解析循环应只跑一遍(一次性把命令全部执行),而不是每帧都重跑。
substr 取出的是字符串,必须 int(x) 转数字,否则位置计算出错、或 + 变成字符串拼接。range(0, len(message), 5);漏掉步长 5 会逐字符乱解析。substr(i, 2);写成 substr(i, 1) 只取到一位,坐标就错了。index+1、y 从 index+3 开始;偏移量数错会把类型和坐标错位。"p"(农民)与 "P"(圣骑士)是不同键,别写混。len-1 位;随便挑一个会派错单位。while True 里反复召唤耗光金币。恭喜你成为一名合格的"密令译电员"!在封神冰川的这一单元,你学会了字符串是有序的字符序列、每个字符都有从 0 起的索引;掌握了用 substr(start, length) 按位置截取子串,把一条 Txxyy 命令拆成类型、x、y 三部分;学会用字典把类型字母优雅地映射成兵种名,替代冗长的 if 判断;理解了固定格式命令为何能靠步长为 5 的循环逐条解析;并用 hero.summon 召唤单位、用 int+Vector 转换坐标、用 hero.command 把最新召唤的单位精确送到目的地。在"编码订单"一关里,你把这一整套字符串解析流程融会贯通。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应写法 |
|---|---|---|
| 读取消息 | 从指令牌取命令串 | sign.message |
| 字符串截取 | 按起点+长度取子串 | message.substr(i, 2) |
| 字典映射 | 字母→兵种名 | summonTypes["a"] |
| 固定格式 | 每条命令 5 字符 | Txxyy |
| 步长循环 | 每次前进 5 | range(0, len(m), 5) |
| 字符串转数字 | 坐标转整数 | int(x) |
| 召唤单位 | 花金币变出兵种 | hero.summon(type) |
| 指挥移动 | 最新单位送到坐标 | command(u, "move", Vector(...)) |
能把一串冰冷的密文,翻译成一支各就各位的大军——你已经掌握了程序员处理数据的看家本领。
封神冰川的试炼到此圆满,愿你在代码的世界里继续封神!🧊❄️⚔️
扣哒世界 编程知识 · 计算机科学 6(封神冰川):第六单元
扣哒世界教研团队