欢迎来到计算机科学 5 的第十四单元!到目前为止,你已经能熟练使用普通数组(一维列表)——把一排数据整整齐齐排成一行。但真实世界里有太多东西不是"一排"能装下的:棋盘、地图、屏幕上的像素、Excel 表格……它们都是有行有列的方格世界。本单元你将掌握描述这类结构的利器——二维数组(2D array),也叫嵌套数组(nested array)。
grid[i][j] 读取第 i 行第 j 列的元素,用双层 for 循环把整张网格从头扫到尾,再根据每个格子的值(正数 / 1 / 0)决定英雄该做什么,最后把二维的"行列索引"翻译成游戏世界里真实的笛卡尔 (x, y) 坐标。两关"力量的源点"与"危险谷"就是这套技能的完整演练。
关卡"危险谷"的概览一句话点破本质:"嵌套数组,或二维数组,是一种……包含更多数组的数组!"普通数组里装的是数字或字符串,而二维数组里装的每一个元素本身又是一个数组。看下面这个最简单的例子:
# 挤成一行时看着复杂 doubleArray = [[1, 2], [3, 4]] # 整理换行后,结构一目了然 doubleArray = [ [1, 2], # ← 第 0 行,本身是一个数组 [3, 4] # ← 第 1 行,也是一个数组 ]
[[1,2],[3,4]] 和分行写法完全等价,但后者一眼就能看出"两行两列"。
理解二维数组的关键,是把它想象成一张表格:最外层数组的每个元素代表一行(row);每一行内部的数组元素则代表这一行里的各个列(column / cell)。"力量的源点"关卡里巫师给的地图正是这样一张更大的网格:
powerMap = [ [0, 1, 0, 0, 0], # 第 0 行 [2, 0, 0, 3, 0], # 第 1 行 [0, 0, 4, 0, 0], # 第 2 行 [0, 0, 0, 0, 5] # 第 3 行 ]
grid[行][列]。行的索引通常用 i,列的索引通常用 j。记住这个顺序,后面读值、遍历都不会乱。
读普通数组用一个方括号 arr[i];读二维数组要用两个方括号:第一个选出"哪一行",第二个在这一行里选出"哪一列"。也就是 grid[i][j] = 第 i 行第 j 列的值。它的运作可以拆成两步理解。
"危险谷"关卡展示了访问元素的两种写法,效果完全相同。第一种是分步取值:先用一个索引把某一行整个取出来存进变量,再用第二个索引从这行里取具体元素——这样代码更清晰,也方便复用整行。第二种是叠加索引:把两个方括号连着写,一步到位。
doubleArray = [[1,2], [3,4]] row1 = doubleArray[0] # row1 是 [1, 2] cell1 = row1[0] # cell1 是 1
doubleArray = [[1,2], [3,4]] cell1 = doubleArray[0][0] # cell1 直接就是 1
doubleArray[0],它是第 0 行 [1, 2];再对这个结果取 [0],得到 1。两个方括号是从左到右依次生效的,就像连续开两道门。
要把整张网格的每个格子都访问一遍,一层循环不够——必须用双层(嵌套)循环。外层循环负责"走过每一行",内层循环负责"走过当前这一行里的每一列"。关卡的思路是:外层用 range(len(powerMap)) 遍历所有行的索引 i;对每一行,内层再用 range(len(powerMap[i])) 遍历这一行里所有列的索引 j。
# 首先,循环遍历行的索引 for i in range(len(powerMap)): # 每一行都是一个数组,遍历它的列索引 for j in range(len(powerMap[i])): # 获取 i 行 j 列的值 pointValue = powerMap[i][j] # ...根据 pointValue 决定做什么...
i 行;内层循环 = 在这一行里从左到右逐个字读到底 j。读完一行,外层循环自动翻到下一行,再从头逐格读起——直到整张网格读完。这正是计算机里最经典的"行主序(row-major order)"遍历方式。
为什么用 range(len(powerMap)) 而不是写死 range(4)?因为 len(powerMap) 会自动算出"有几行",len(powerMap[i]) 会算出"第 i 行有几列"。这样无论地图是 4×5 还是 10×10,同一段代码都能正确扫完,不用手动改数字。这是编写健壮遍历代码的关键习惯。
二维数组真正的威力,在于把格子里的数值当作指令来解读。遍历到每个格子时,用 if 判断它的值,不同的值触发不同的行动。这一单元的两关正好演示了两种典型约定:
0 是"不好的点",跳过不理;任何正数都是"力量源点",要移动过去念咒 "VENI" 迎接挑战。
子数组里 0 是农民(不能伤害),1 是食人魔(要布置火焰陷阱)。只在值为 1 的格子建陷阱。
用代码表达这种"看值办事"的逻辑,核心就是在双层循环最里面加一个 if:
pointValue = powerMap[i][j] if pointValue > 0: # 力量的源点:正数才是好点 # ...在这里执行"念咒 + 应战"... pass # 或者,危险谷的判断: if tile is 1: # 值为 1 表示食人魔 # ...在这里"建造火焰陷阱"... pass
if tile is 1:。在 扣哒世界 环境中它能正常工作,但在标准 Python 里判断数字是否相等更推荐写 if tile == 1:。is 比较的是"是不是同一个对象",== 比较的是"值是否相等"——对数字判断,== 才是稳妥写法。
这是本单元最"数学"的一步,也是最容易搞混的地方。i 和 j 只是网格里的行号、列号(从 0 开始的整数),它们不是英雄要走到的真实位置。游戏世界用的是笛卡尔坐标 (x, y)(以像素为单位)。要让英雄站到某个格子上,必须先把 (行, 列) 换算成 (x, y)。
"力量的源点"贴心地准备了一个转换函数。它的逻辑是:从一个起始偏移量开始,每往右一列 x 就增加固定间距,每往下一行 y 就增加固定间距。
# 该函数把网格的 (行, 列) 转换为 x-y 坐标 def convert(row, col): return {'x': 16 + col * 12, 'y': 16 + row * 12}
hero.buildXY("fire-trap", 36 + 6*j, 20 + 6*i)。同样是"起始偏移 + 索引 × 间距"的套路——j 控制横向 x,i 控制纵向 y,只是偏移量和间距的具体数字不同。理解了公式结构,换任何网格都能举一反三。
hero.moveXY(i, j)。i=1, j=3 只是"第 1 行第 3 列",直接当坐标会让英雄走到地图角落的 (1, 3) 像素处。一定要先经过转换公式,得到真正的 (x, y) 再移动。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关的通关审判是打败 3 个黑暗生物,但它们藏在力量源点上,需要你先找到这些点。巫师把整张地图交给你,就是变量 powerMap——一个二维数字数组,0 是危险的错误位置,任何正数都是安全的力量源点。策略非常清晰:用双层循环把地图每个格子扫一遍,遇到正数就走过去念咒 "VENI" 召唤/迎战,然后清理出现的敌人、捡起掉落的物品。
算法分四步。第一,拿到地图:先 findNearest(findFriends()) 找到巫师,再读取他的 wizard.powerMap。第二,双层遍历:外层 for i in range(len(powerMap)) 走行,内层 for j in range(len(powerMap[i])) 走列,取出 powerMap[i][j]。第三,按值分支:if powerMap[i][j] > 0 才处理这个格子。第四,坐标转换 + 行动:用 convert(i, j) 把行列换成 (x, y),moveXY 过去、say("veni"),再打退出现的敌人、捡走物品。
# 关键片段:遍历地图 + 按值分支 + 坐标转换 for i in range(len(powerMap)): for j in range(len(powerMap[i])): if powerMap[i][j] > 0: # 正数 = 力量源点 coor = convert(i, j) # 行列 → x,y hero.moveXY(coor['x'], coor['y']) hero.say("veni") # 念出咒语 # ...随后清理敌人、捡起物品...
"VENI" 都可能召唤出敌人或物品。念咒之后加一个 while enemy and enemy.health > 0: hero.attack(enemy) 把敌人打倒,再 findNearest(findItems()) 捡起掉落物,这样才能安全推进到下一个源点。
if 负责决定去不去,转换函数负责翻译坐标。这种分层思路正是处理网格类问题的通用模板。
食人魔劫持了农民做人质,你要营救他们。侦察兵已经先行摸清了敌情,把整个山谷的布局报告成一个嵌套数组 hero.grid:子数组里 0 是农民(绝不能在那儿放陷阱),1 是食人魔(要在那儿建 "fire-trap")。策略是:遍历整张 grid,只在值为 1 的位置精准布置火焰陷阱,布完后退到掩体后,让冲进来的食人魔自投罗网。
算法三步走。第一,外层遍历行:for i in range(len(hero.grid)),并把当前行存进变量 row = hero.grid[i]——记住"被包含的数组就是一个数组",row 本身还能继续遍历。第二,内层遍历列:for j in range(len(row)),取出 tile = row[j]。第三,判断并布雷:if tile == 1 就用坐标公式 buildXY("fire-trap", 36 + 6*j, 20 + 6*i) 建陷阱。全部布完,moveXY(29, 55) 撤到安全的掩体后方。
# 关键片段:分步取行 + 内层判断 + 内联坐标公式 for i in range(len(hero.grid)): row = hero.grid[i] # row 也是一个数组! for j in range(len(row)): tile = row[j] if tile == 1: # 1 = 食人魔 hero.buildXY("fire-trap", 36 + 6*j, 20 + 6*i) hero.moveXY(29, 55) # 最后撤到掩体后
if 判断必须严格锁定 tile == 1,只对食人魔布雷。少了这个判断、或写错成 0,就会把火焰陷阱建在农民脚下——救人反成害人。
row = hero.grid[i] 再遍历 row,而不是全程用 hero.grid[i][j]。把整行存进变量后,内层循环读起来更直观,也少写一层方括号——这是处理二维数组时值得养成的清晰写法。
grid[行][列] 即 grid[i][j],把 i、j 写反会读到完全错误的格子甚至越界。moveXY(i, j) 是错的;i、j 是行列号,必须先用转换公式换成真实的 (x, y)。len(powerMap[i]) 或 len(row),写死 range(5) 换张地图就会越界或漏格。if 判断值(正数 / 1 / 0),无脑对所有格子行动会误伤农民或跑到坏点上。j 决定、纵向 y 由行 i 决定;把 i、j 在公式里放反会让陷阱/移动全部错位。is 与 == 混用——判断数字相等在标准 Python 里请用 ==,is 只适合判断"是否同一对象"。grid[i][j] 时 i、j 不能超过各自的长度,靠 range(len(...)) 遍历天然避免越界。恭喜你解锁了描述"网格世界"的核心数据结构——二维数组!这一单元你理解了它的本质:数组的数组,外层是行、内层是列。你学会了用 grid[i][j] 精确读取任意格子,用双层 for 循环按行主序扫遍整张网格,用 if 根据格子的值(正数 / 1 / 0)做出不同决策,还掌握了把行列索引换算成笛卡尔 (x, y) 坐标的公式套路。在"力量的源点"里你遍历巫师地图、找出所有力量源点应战,在"危险谷"里你照着侦察情报精准布雷、避开人质。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 二维数组结构 | 数组里每个元素又是数组 | 两关 |
| 行与列 | 外层是行 i,内层是列 j | 两关 |
| 读取元素 | grid[i][j] 双重方括号 | 两关 |
| 分步 vs 叠加 | 先取行再取列,或一步到位 | 危险谷 |
| 双层循环遍历 | 外循环走行、内循环走列 | 两关 |
| len 自适应 | range(len(...)) 不写死尺寸 | 两关 |
| 按值分支 | 正数/1 才行动,0 跳过 | 两关 |
| 坐标转换 | 偏移 + 索引×间距 → (x, y) | 力量的源点 |
懂得把整个世界读成一张网格、逐行逐列扫描并按值决策的英雄,才是云峰之上真正洞察全局的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🗺️🔥
扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十四单元
扣哒世界教研团队