去做题

第六单元:字符串算法 · 解码部署命令

Unit 6: String Algorithms — Decoding Deployment Orders
计算机科学 6 · 封神冰川 Deified Glacier · 编码订单 Coded Orders

6.1 字符串算法概述

Overview of String Algorithms

欢迎来到封神冰川的最后一个单元!在这片被冰雪封印的战场上,你的指挥总部下达了一套加密的部署命令——所有指令被压缩进了一根指令牌(sign)的一长串文本里。你不能再靠肉眼一个个数坐标,而要让程序自动读懂这串文本、召唤正确的兵种、并把它们精确送到指定坐标。这正是字符串算法(string algorithms)大显身手的地方:把一段看似杂乱的字符,按照约定的规则拆解成有意义的信息。

本单元主线:理解字符串是有序的字符序列;用 substr(字符串截取)按索引取出子串;用字典(dict)把类型字母映射成兵种名;识别 Txxyy 这种固定格式命令;用步长为 5 的循环逐条解析整条消息;最后用 hero.summon 召唤单位、hero.command 把单位移动到解析出的坐标。

6.1.1 什么是字符串(What Is a String)

6.1.1.1 字符串是"排好队的字符" A string is an ordered sequence of characters

字符串就是一串按顺序排列的字符——字母、数字、符号都算。它和数组很像:每个字符都有一个索引(index),从 0 开始编号。比如字符串 "a1536" 里,第 0 个字符是 a,第 1 个是 1,第 2 个是 5,以此类推。正是因为字符有固定的位置,我们才能"按位置"把想要的部分精准截取出来。

字符串 "a1536" 的每个字符都有索引 a 1 5 3 6 01234

6.1.2 读取指令牌信息(Reading the Sign's Message)

6.1.2.1 从 sign 上拿到那串命令 Get the command string from the sign

关卡里的部署命令写在场上的一块指令牌(sign)上。先用 hero.findByType("sign") 找到它,取出第一块(索引 [0]),再读取它的 .message 属性——这就是我们要解码的那一长串文本。

# 找到指令牌,读取上面的部署命令
sign = hero.findByType("sign")[0]
message = sign.message
# message 现在是类似 "a1536s4532P2455..." 的一长串字符
解析(parse)是什么意思:解析就是"按照约定的规则,把一大段文本拆成一小块一小块有意义的信息"。这里我们要把 message 拆成一条条 Txxyy 命令,再从每条里分离出兵种类型和坐标。学会解析,是处理真实世界数据(配置文件、网络协议、日志)的基本功。

6.2 字符串截取 substr

Slicing Strings with substr

6.2.1 索引与截取(Index and Slice)

6.2.1.1 substr(起点, 长度) 取出一段子串 substr(start, length) returns a substring

字符串截取用 substr(start, length):从索引 start 处开始,取 length 个字符,返回一个新的子字符串。它不改变原字符串,只是"复制出"其中一段。这是本单元最核心的工具——正是靠它,我们才能把 Txxyy 拆成 T、xx、yy 三部分。

message.substr(起点, 长度) message . substr (index, 2) 从哪个字符串取 从 index 起取 2 个字符
substr 与索引的区别:message[i] 只取一个字符;而 message.substr(i, n) 能取连续 n 个字符。坐标 xx 是两位数,必须用 substr(i, 2) 一次取两个字符,才能得到完整的 "15" 而不是单个 "1"。

6.2.2 取出类型与坐标(Extracting Type and Coordinates)

6.2.2.1 一条 Txxyy 命令怎么拆 How to split one Txxyy command

假设某条命令从索引 index 开始。类型字母 T 在第 index 位,取 1 个;x 坐标在第 index+1 位,取 2 个;y 坐标在第 index+3 位,取 2 个。三次 substr 就把一条命令拆得清清楚楚。

aindex 1+1 5+2 3+3 6+4
■ 类型 substr(index,1) = "a"  ·  ■ x坐标 substr(index+1,2) = "15"  ·  ■ y坐标 substr(index+3,2) = "36"
# 从 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",而不是我们要的坐标。

6.3 字典映射字符到兵种

Mapping Characters to Unit Types with a Dict

6.3.1 字符→兵种(Character to Unit Type)

6.3.1.1 用字典建立"字母↔兵种"的对照表 Build a lookup table from letter to unit type

命令里的类型是单个字母(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)是两个不同的键!字典和字符串都严格区分大小写,抄写命令表时千万别把它们混为一谈。

6.3.2 用键取值(Looking Up by Key)

6.3.2.1 中括号一查,字母变兵种 Use square brackets to look up a value

建好字典后,用 字典[键] 的写法就能瞬间查到对应的值。比如解析出类型字母是 "a",那么 summonTypes["a"] 立刻返回 "archer"。这比一长串 if 判断简洁得多,而且新增兵种时只要往字典里加一行即可。

# 用解析出的字母作键,查出完整兵种名
letter = "a"
unitType = summonTypes[letter]   # 得到 "archer"
hero.summon(unitType)
📖 像查字典一样
想象一本词典:你翻到 "a" 这一页,上面写着解释 "archer"。字典(dict)在编程里做的正是这件事——给它一个"词条"(键),它立刻返回"释义"(值)。查找几乎是瞬间完成的,比一页页翻找(一个个 if 判断)快得多。

6.4 解析固定格式命令

Parsing Fixed-Format Commands

6.4.1 Txxyy 格式(The Txxyy Format)

6.4.1.1 每条命令恰好 5 个字符 Each command is exactly 5 characters

整条消息由若干条命令首尾相接拼成,每条命令都严格遵守 Txxyy 这个固定格式(fixed format):1 个类型字母 + 2 位 x 坐标 + 2 位 y 坐标,正好 5 个字符。正因为长度固定,我们才能靠"数格子"精确定位每条命令的起点——第 1 条从 0 开始,第 2 条从 5 开始,第 3 条从 10 开始……

"a1536 s4532 P2455 ..." 每 5 个字符一条命令 a1536 s4532 P2455 起点 0 起点 5 起点 10

6.4.2 步长为 5 的循环(Looping with a Step of 5)

6.4.2.1 range(起点, 终点, 步长) 中的步长 The step in range(start, stop, step)

要每次跳到下一条命令的起点,就得让循环变量每轮增加 5 而不是 1。range 的第三个参数正是步长(step):range(0, len(message), 5) 会依次产生 0, 5, 10, 15, ...——恰好是每条命令的起始索引。这样循环体每跑一次,正好处理一条完整命令。

步长 1(逐字符)
for i in range(0, 15):
    # 0,1,2,3,...,14
    # 每次只前进 1 个字符
步长 5(逐命令)
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):消息可能有很多条命令,我们不知道具体多少条。用 len(message) 作为循环终点,无论消息多长都能自动跑完——这让代码对任意长度的命令串都通用。因为每条命令都是 5 个字符且首尾相接,len(message) 一定是 5 的整数倍。

6.5 召唤与指挥单位

Summoning and Commanding Units

6.5.1 summon 召唤(Summoning Units)

6.5.1.1 hero.summon 花金币变出一个单位 hero.summon spends gold to create a unit

hero.summon("archer") 会消耗一定金币,在你身边召唤出一个弓箭手。召唤需要 Boss Star 权限——不同兵种要求不同等级的 Boss Star(士兵最低,圣骑士、炮兵较高)。召唤前应先用 hero.costOf(type) 查询价格,确认 hero.gold 够用,避免召唤失败。

# 金币足够时才召唤,避免浪费一帧
if hero.gold > hero.costOf(type):
    hero.summon(type)
Boss Star 的分级:关卡说明提到,要召唤并指挥所有兵种(含圣骑士、炮兵)需要 Boss Star IV;如果你只有 Boss Star III,也能召唤基础兵种(士兵、弓箭手、狮鹫等)完成关卡的基础形式。装备等级越高,可指挥的兵种越丰富。

6.5.2 command 移动(Commanding Units to Move)

6.5.2.1 召唤后立刻找到"最新那个"并派它去坐标 Find the just-summoned unit and send it

召唤只是把单位变出来,它还停在你身边。要把它送到指定坐标,得用 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](即最后一个元素)总是最新那一个。指挥它移动,就不会误派到之前已经就位的同类单位。

6.5.3 int 与 Vector(Converting to Numbers and Positions)

6.5.3.1 字符串坐标要先转数字,再包成向量 Convert string coordinates to a Vector

substr 得到的 x、y 还是文本(如 "15"、"36")。移动指令需要的是数字坐标,所以要用 int() 把它们转成整数,再用 Vector(int(x), int(y)) 打包成一个位置向量传给 command。

"15", "36" int() → 15, 36 Vector(15, 36)
忘了 int 会出错:如果直接把字符串 "15" 当坐标,程序无法正确计算位置;而且用 + 连接两个字符串是拼接不是相加("1"+"5" 得到 "15" 而非 6)。所以取出的坐标一定要 int() 转成数字后再使用。

6.6 实战关卡精讲

Level Strategy & Analysis

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

第 15 关 · 编码订单

🧊 编码订单 —— 解码消息、召唤并部署军队

扣哒世界 编码订单关卡 - koudashijie.com
图2:编码订单——指令牌上写着一长串 Txxyy 加密命令,解码后召唤对应兵种并送往冰川上的指定坐标

策略:读牌 → 解析 → 召唤 → 部署

本关的部署命令全部编码在一块指令牌里,形如 "a1536s4532P2455..."。你的任务是把这串"密文"翻译成实际行动。整体策略分四步:读牌——从 sign.message 取出命令串;解析——以步长 5 逐条拆出类型和坐标;召唤——用字典把字母翻译成兵种名再 summon;部署——把刚召唤的单位 command 移动到解析出的坐标。

① 读牌 ② 解析 ③ 召唤 ④ 部署

思路拆解一:用字典代替一堆 if

解析出来的类型是字母,但召唤需要兵种全名。与其写六个 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 的主循环把一切串起来

最后,用步长为 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 里而不加控制,会不断重复召唤同一批单位、瞬间耗光金币。本关的解析循环应只跑一遍(一次性把命令全部执行),而不是每帧都重跑。

6.7 常见错误

Common Mistakes

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

6.7.1.1 字符串解析的典型问题 Typical issues with string parsing

扣哒世界 字符串算法常见错误速查 - koudashijie.com
图3:忘转 int、步长写错、substr 长度取错、字典大小写混淆,是本单元最常见的失误
易错点清单:
  • 坐标忘转 int——substr 取出的是字符串,必须 int(x) 转数字,否则位置计算出错、或 + 变成字符串拼接。
  • 步长写错——遍历命令要用 range(0, len(message), 5);漏掉步长 5 会逐字符乱解析。
  • substr 长度取错——坐标是两位数要 substr(i, 2);写成 substr(i, 1) 只取到一位,坐标就错了。
  • 索引偏移算错——x 从 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
步长循环每次前进 5range(0, len(m), 5)
字符串转数字坐标转整数int(x)
召唤单位花金币变出兵种hero.summon(type)
指挥移动最新单位送到坐标command(u, "move", Vector(...))

能把一串冰冷的密文,翻译成一支各就各位的大军——你已经掌握了程序员处理数据的看家本领。
封神冰川的试炼到此圆满,愿你在代码的世界里继续封神!🧊❄️⚔️

扣哒世界 编程知识 · 计算机科学 6(封神冰川):第六单元

扣哒世界教研团队