去做题

第十七单元:几何计算 · 周长面积与近似比较

Unit 17: Geometry — Perimeter, Area & Approximate Comparison
计算机科学 5 · 安息云峰 Cloudrip Mountain · 矩形仪式 → 方盾 → 雪人面积

17.1 用代码算几何

Doing Geometry with Code

欢迎来到计算机科学 5 的第十七单元!在这一单元里,几何不再是纸上的公式,而是拯救村庄的魔法。你要用代码计算矩形的周长与面积、用坐标偏移把圣骑士摆成正方形、用圆面积公式为天选之子注入呐喊之力。更重要的是,你会遇到一个真实编程中无处不在的难题——计算机对小数的处理并不完美,于是你将学到专业程序员的应对之道:近似比较(approximate comparison)。

本单元主线:把几何公式写成 Python 函数——矩形周长 (a+b)×2、矩形面积 a×b、圆面积 π×r²;用坐标偏移由已知角点算出正方形的其余顶点;并用3% 容差的 almostEqual 函数解决浮点数"几乎相等但不完全相等"的比较难题。

17.1.1 坐标与距离(Coordinates and Distance)

17.1.1.1 位置是 (x, y),距离用 distanceTo Positions are (x, y); measure with distanceTo

游戏世界里每个单位都有一个位置 pos,包含横坐标 pos.x 和纵坐标 pos.y。两点之间的距离可以直接用 distanceTo 测量。本单元的几何计算,正是建立在"用坐标表示位置、用距离表示边长"之上——比如把两个圣骑士之间的距离当作矩形的一条边。

# 用某个单位作为测量的"基准点"
base = hero.findNearest(hero.findFriends())

# 测量基准点到两个圣骑士的距离,作为矩形的两条边
sideSN = base.distanceTo("Femae")      # 南北方向的边
sideWE = base.distanceTo("Illumina")  # 东西方向的边

17.1.2 用函数封装公式(Wrapping Formulas in Functions)

17.1.2.1 一个公式,一个函数 One formula, one function

回忆第一单元学的函数——把"做某件事"的代码打包命名。几何公式是函数的绝佳用武之地:把"算周长"写成 perimeter(a, b)、"算面积"写成 area(a, b),之后需要时喊一声名字、传入边长,就能拿到结果。这让主循环里的代码清晰得像数学公式本身。别忘了函数要用 return 把算出的结果返回出来。

return 才有结果:几何函数光算出值还不够,必须用 return 把结果送回调用处,否则函数默认返回 None。比如 return side1 * side2——算出面积并交回去。

17.2 矩形的周长与面积

Perimeter and Area of a Rectangle

17.2.1 周长 (a+b)×2(Perimeter)

17.2.1.1 四条边加起来 Sum of all four sides

矩形有四条边,对边相等:两条长为 a、两条长为 b。所以周长就是四条边的总和 a + a + b + b,可以更简洁地写成 a×2 + b×2,或者提取公因数写成 (a + b)×2——三种写法完全等价,选你觉得最清晰的即可。关卡"矩形仪式"要求你补全这个函数。

# 周长 = 四条边之和
def perimeter(side1, side2):
    return side1 * 2 + side2 * 2
    # 等价写法:return (side1 + side2) * 2

17.2.2 面积 a×b(Area)

17.2.2.1 相邻两边相乘 Multiply two adjacent sides

矩形的面积是两条相邻边的乘积 a × b。注意是"相邻"两边(一长一宽),不是两条对边。这个公式最简单,一行 return 就够了。

# 面积 = 相邻两边相乘
def area(side1, side2):
    return side1 * side2
矩形:周长 (a+b)×2,面积 a×b 面积 = a × b a a b b
周长 perimeter
def perimeter(a, b):
    return a*2 + b*2
面积 area
def area(a, b):
    return a * b

17.3 近似比较容错

Approximate Comparison (Tolerance)

17.3.1 为何需要容错(Why Tolerance Is Needed)

17.3.1.1 计算机算不准小数 Computers can't represent decimals exactly

关卡"矩形仪式"点破了一个关键真相:"计算机很难处理精确的数字。"这有两层原因:其一,计算机用二进制存储小数,很多十进制小数(如 0.1)根本无法精确表示,运算后会产生微小误差(这叫浮点误差);其二,圣骑士在不停移动,边长时刻在变,几乎不可能刚好卡在某个精确值上。如果你用 == 去判断"面积是否等于 660",可能永远也等不到那一刻。

🎯 精确相等 vs 差不多就行
要求你"把水倒到正好 500.000000 毫升"几乎不可能——手一抖就多了或少了。但"倒到 500 毫升左右,误差别超过 3%"就很现实。程序判断移动中的目标值时也一样:用一个容差范围代替精确相等,才切合实际。

17.3.2 3% 误差算法(The 3% Tolerance Algorithm)

17.3.2.1 落在 0.97 倍到 1.03 倍之间就算相等 Within 0.97× to 1.03× counts as equal

解决办法是写一个"近似相等"函数 almostEqual。它不要求两个值分毫不差,而是检查一个值是否落在另一个值的 97% 到 103% 区间内——也就是允许上下 3% 的误差。关卡给出的核心判断是:valueA > 0.97 * valueB and valueA < 1.03 * valueB。两个条件用 and 连接,必须同时满足才算"几乎相等"。

# 在 3% 误差内比较 A 和 B
def almostEqual(valueA, valueB):
    # A 大于 B 的 0.97 倍,且小于 B 的 1.03 倍
    if valueA > 0.97 * valueB and valueA < 1.03 * valueB:
        return True
    # 否则退回到精确相等判断
    return valueA == valueB
3% 容差:落在绿色区间就算"几乎相等" 目标值 B 0.97×B 1.03×B 太小 ✗ 太大 ✗
另一种等价写法:关卡还给了用 min/max 的思路:先算出 min = numberA * 0.97 和 max = numberA * 1.03,再判断 numberB 是否 > min and < max。本质完全一样——都是"围绕目标值画一个 ±3% 的窗口,看另一个值有没有落进去"。
别用 == 比较小数:这是编程里的经典陷阱。由于浮点误差,0.1 + 0.2 == 0.3 在很多语言里竟然是 False!所以凡是涉及小数(尤其是距离、面积这类计算结果)的相等判断,都应该用"近似相等"而不是 ==。

17.4 坐标偏移算顶点

Computing Vertices by Coordinate Offset

17.4.1 正方形的性质(Properties of a Square)

17.4.1.1 邻角共享坐标、四边等长 Adjacent corners share a coordinate; all sides equal

关卡"方盾"要你把圣骑士摆成正方形。要算出顶点坐标,先要吃透正方形的两条性质:"正方形的角与邻近的角共享 x 或 y 坐标",以及"正方形是等边的"。也就是说,如果左上角在 (x, y),右上角与它共享同一个 y(只是 x 增加了一个边长);左下角与它共享同一个 x(只是 y 变化了一个边长)。

邻角共享一个坐标,相差一个 sideLength 左上(x,y) 右上(x+s, y) 左下(x, y-s) 右下(x+s, y-s) s = sideLength

17.4.2 用偏移求角点(Finding Corners via Offset)

17.4.2.1 已知一角,加减边长得其余角 From one known corner, add/subtract the side length

核心算法叫坐标偏移:以一个已知角点(比如左上角 celadia)为基准,通过给它的 x 或 y 加减一个 sideLength,就能算出其他角点的位置。在"方盾"里,两位圣骑士已在上方两角,你要算出下方两角:左下角与左上角共享 x、y 减去边长;右下角的 x 是左上角 x 加边长、y 同样减去边长。

# 左下角:x 与左上角相同,y 向下偏移一个边长
leftBottom = {"x": celadia.pos.x,
              "y": celadia.pos.y - sideLength}

# 右下角:x 偏移一个边长,y 也向下偏移一个边长
rightBottom = {"x": celadia.pos.x + sideLength,
               "y": celadia.pos.y - sideLength}
边长哪里来:正方形四边等长,所以只要量出上边两角的距离,就得到了边长:sideLength = celadia.distanceTo(dedalia)。这个边长既决定了正方形有多大,也是所有偏移量的来源。

17.5 圆的面积与 π

Circle Area and π

17.5.1 π×r² 公式(The πr² Formula)

17.5.1.1 半径的平方乘以 π Pi times radius squared

关卡"雪人面积"用到圆面积公式:半径为 r 的圆,面积等于 π × r × r(即 π×r²)。在游戏里,天选之子呐喊的力度正好等于这个圆的面积,而半径就是他到最近敌人的距离。写成代码就是把半径乘两次、再乘以 π。

# 半径 = 天选之子到最近敌人的距离
radius = chosen.distanceTo(chosen.findNearestEnemy())

# 圆的面积 = π × r × r
area = radius * radius * Math.PI
圆面积 = π × r² r area = π·r·r

17.5.2 Math.PI 常量(The Math.PI Constant)

17.5.2.1 内置的圆周率,不用自己写 A built-in constant for pi

π 是一个无限不循环小数,约等于 3.14159……你不需要自己敲一长串数字,游戏提供了内置常量 Math.PI,它就代表这个精确的圆周率。用它比手写 3.14 更准确、更清晰。关卡说明也强调:π"等于任何圆周的周长与其直径的比值",是数学中最著名的常数之一。

# 直接用内置常量 Math.PI,别手写 3.14
area = Math.PI * radius * radius   # 更精确

# hero.say(Math.PI)  # 大约 3.141592653589793
常量 vs 变量:Math.PI 是一个"常量"——它的值固定不变,永远是圆周率。用常量代替写死的数字("魔法数字"),能让代码更易读、更不容易出错。这是良好的编程习惯。

17.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 矩形仪式

📐 矩形仪式 —— 周长面积 + 近似比较

扣哒世界 矩形仪式关卡 - koudashijie.com
图2:矩形仪式——四位圣骑士不断移动,当他们围出的矩形周长与面积都接近目标值时喊出咒语 VENITE

策略:持续监测,近似达标就念咒

本关是本单元的核心关。四位圣骑士会不停移动,你要在他们恰好围出指定周长(104)和指定面积(660)的矩形时喊出 "VENITE!" 召唤守护者。由于骑士一直在动、且计算机算不准小数,精确相等几乎不可能,所以策略是:在 while 循环里持续测量、用近似比较判断,一旦周长和面积都落在 3% 误差内就立刻念咒。

思路拆解

算法分三部分。第一,补全两个几何函数:perimeter 返回 side1*2 + side2*2,area 返回 side1 * side2。第二,在循环里实时计算:每一帧用 distanceTo 量出当前两条边长 sideSN、sideWE,代入函数得到 currentPerimeter 和 currentArea。第三,用近似比较双重判断:同时用 almostEqual 检查当前面积接近目标面积、当前周长接近目标周长,两个条件用 and 连接,都满足才 say("VENITE!") 并 break。

# 关键片段:循环监测 + 近似双重判断
while True:
    sideSN = base.distanceTo("Femae")
    sideWE = base.distanceTo("Illumina")
    currentPerimeter = perimeter(sideSN, sideWE)
    currentArea = area(sideSN, sideWE)
    if almostEqual(currentArea, requiredArea) and \
       almostEqual(currentPerimeter, requiredPerimeter):
        hero.say("VENITE!")
        break
为什么两个条件都要查:只有周长对、或只有面积对都不够——一个瘦长的矩形和一个正方形可能面积相同但周长不同。周长和面积同时达标,才能唯一确定矩形的形状,所以必须用 and 把两个 almostEqual 连起来。
别用 == 判断:如果把 almostEqual 换成 currentArea == 660,由于骑士移动和浮点误差,这个条件几乎永远为假,程序会陷入死循环、咒语永远念不出来。这正是本关设计 almostEqual 的用意。
关卡 · 方盾

🛡️ 方盾 —— 坐标偏移求正方形顶点

扣哒世界 方盾关卡 - koudashijie.com
图3:方盾——已知上方两角,用坐标偏移算出下方两角,命令两位圣骑士就位形成方阵挡住雪人

策略:由已知两角补全另外两角

本关要把四位圣骑士摆成正方形,用方阵挡住有侵略性的野雪人。其中 Celadia(左上角)和 Dedalia(右上角)已就位,你要计算并命令 Vaelia(左下角)和 Illumina(右下角)移动到正确位置。策略核心是利用正方形的坐标规律,从已知角点偏移出未知角点。

思路拆解

算法分两步。第一,求边长:上方两角的距离就是边长,sideLength = celadia.distanceTo(dedalia)。第二,用坐标偏移算下方两角并命令移动:左下角与左上角 Celadia 共享 x,y 向下偏移一个边长(y - sideLength);右下角的 x 在 Celadia 基础上加一个边长(x + sideLength),y 同样减一个边长。用 hero.command(单位, "move", 目标坐标字典) 把两位圣骑士派过去。

# 关键片段:坐标偏移 + 命令移动
sideLength = celadia.distanceTo(dedalia)

# Vaelia → 左下角(x 与左上角相同,y 减边长)
hero.command(vaelia, "move",
    {"x": celadia.pos.x, "y": celadia.pos.y - sideLength})

# Illumina → 右下角(x 加边长,y 减边长)
hero.command(illumina, "move",
    {"x": celadia.pos.x + sideLength, "y": celadia.pos.y - sideLength})
findByName 按名字取单位:本关先用一个 findByName 辅助函数遍历 friends、按 id 找到指定名字的圣骑士。这样才能精确拿到 Celadia、Vaelia 等特定单位,再对它们分别下命令。
y 是减不是加:本关下方角点的 y 用的是减边长(y - sideLength),因为在这张地图的坐标系里"下方"对应更小的 y 值。方向的正负取决于坐标系,动手时要根据实际地图确认是加还是减。
关卡 · 雪人面积

⭕ 雪人面积 —— 圆面积公式与 Math.PI

扣哒世界 雪人面积关卡 - koudashijie.com
图4:雪人面积——以到最近敌人的距离为半径算出圆的面积,作为天选之子呐喊的力度,再投入战斗

策略:算出圆面积作为呐喊力度,再战斗

本关是本单元的收尾关,聚焦圆面积公式。天选之子能用呐喊震晕雪人,但呐喊的力度必须等于一个圆的面积,而这个圆的半径就是他到最近敌人的距离。策略很直接:先算出圆面积并"说"出来(作为呐喊力度),然后进入战斗循环持续攻击。

思路拆解

算法分两部分。第一,计算并说出圆面积:用 distanceTo(findNearestEnemy()) 得到半径 radius,代入圆面积公式 area = radius * radius * Math.PI,再用 hero.say(area) 说出来。第二,投入战斗:用循环不断找到最近敌人并攻击,直到敌人被击败——"不要只站在那,开始战斗吧!"

# 关键片段:算圆面积并说出,再进入战斗
chosen = hero.findFriends()[0]
radius = chosen.distanceTo(chosen.findNearestEnemy())
area = radius * radius * Math.PI   # π×r²
hero.say(area)                            # 说出力度值

while True:
    enemy = hero.findNearestEnemy()
    while enemy and enemy.health > 0:
        hero.attack(enemy)
radius*radius 就是 r²:代码里没有专门的"平方"符号时,把半径乘两次 radius * radius 就等于 r²。乘法顺序不影响结果,写成 Math.PI * radius * radius 也完全一样。
先说力度再战斗:本关要先把算出的面积 say 出来(这一步是"注入呐喊力度"的关键),之后才进入攻击循环。顺序不能乱——没说出正确力度,呐喊就不会生效。

17.7 常见错误

Common Mistakes

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

17.7.1.1 几何计算的典型问题 Typical issues with geometry code

扣哒世界 几何计算常见错误速查 - koudashijie.com
图5:用 == 比小数、忘写 return、坐标偏移方向错、手写 3.14 代替 π,是本单元最常见的失误
易错点清单:
  • 用 == 比较小数——距离、面积等浮点结果几乎不可能精确相等,必须用近似比较 almostEqual。
  • 函数忘写 return——perimeter、area 算完必须 return 结果,否则返回 None,后续计算全错。
  • 周长面积公式混淆——周长是 (a+b)×2(相加),面积是 a×b(相乘),别把加号乘号搞反。
  • 只判断一个条件——矩形要周长和面积同时达标,两个 almostEqual 必须用 and 连接。
  • 坐标偏移方向错——下方角点是 y - sideLength 还是 y + sideLength 取决于坐标系,方向弄反正方形就歪了。
  • 邻角坐标没对齐——左下角必须与左上角共享 x,写错就形不成正方形。
  • 手写 3.14 代替 π——用内置常量 Math.PI 更精确、更清晰,别自己敲近似值。
  • 把 radius * radius 写成 radius * 2——平方是自乘一次(r×r),不是乘以 2。

本单元小结 ✨

恭喜你把数学几何变成了实实在在的代码魔法!这一单元你学会了用函数封装几何公式——矩形周长 (a+b)×2、矩形面积 a×b、圆面积 π×r²;掌握了一个真实编程中至关重要的技巧——用 3% 容差的近似比较解决浮点数与移动目标"无法精确相等"的难题;还学会了用坐标偏移从已知角点推算正方形的其余顶点,并认识了内置常量 Math.PI。在"矩形仪式、方盾、雪人面积"三关里,你把几何知识、函数封装和条件判断融会贯通。来回顾一下核心概念:

核心概念关键点对应关卡
矩形周长(a+b)×2 或 a*2+b*2矩形仪式
矩形面积相邻两边相乘 a×b矩形仪式
近似比较容错落在 0.97~1.03 倍即视为相等矩形仪式
避免 == 比小数浮点误差,用 almostEqual矩形仪式
正方形性质邻角共享坐标、四边等长方盾
坐标偏移算顶点已知角 ± sideLength 得其余角方盾
圆的面积π × r × r雪人面积
Math.PI 常量内置圆周率,胜过手写 3.14雪人面积

懂得把几何公式写成代码、用容差驯服不精确的小数、用坐标偏移精准布阵的英雄,才是云峰之上真正睿智的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️📐⭕

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十七单元

扣哒世界教研团队