mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2012 字
5 分钟
CS61A 三连:Hog、Cats 与 Ants——从函数式到面向对象

CS61A 是 UC Berkeley 的计算机科学入门神课,核心思想是「抽象」——用函数抽象流程,用数据抽象结构,用对象抽象世界。它的三个项目恰好对应这三个层次,难度递进:Hog(纯函数式:骰子游戏模拟与策略)、Cats(函数 + 数据:打字测试与自动纠错)、Ants(面向对象:蚂蚁 vs 蜜蜂塔防)。本文按完成顺序记录每个项目的规则、关键设计与踩过的坑。

项目概览#

项目主题核心知识点完成时间
Hog骰子对局模拟函数抽象、高阶函数、蒙特卡洛实验2025-03-23
Cats打字测试 + 自动纠错高阶函数、字符串处理、编辑距离、递归2025-03-30
Ants蚂蚁 vs 蜜蜂塔防面向对象、继承、多态、类属性2025-04-15

三个项目都用 doctest + ok 自动评分 验证正确性,代码量递增(375 / 490 / 916 行),这也正是「抽象层级」递增的过程。

一、Hog:函数即策略#

Hog 是一个双人骰子游戏,先到 100 分者胜。每个回合玩家决定掷几个骰子(0-10),得分由三条规则决定:

规则说明
Pig Out掷出的骰子中出现 1,本回合只得 1 分(其余点数作废)
Boar Brawl掷 0 个骰子:得 `max(3 ×
Sus Fuss回合结束时若分数恰有 3 或 4 个因子,跳到下一个质数

模拟器:把规则做成可注入的参数#

Phase 1 的精华在 play 的签名——游戏规则本身是参数

def play(strategy0, strategy1, update,
score0=0, score1=0, dice=six_sided, goal=GOAL):
"""Simulate a game and return the final scores of both players..."""
who = 0
while score0 < goal and score1 < goal:
if who == 0:
score0 = update(strategy0(score0, score1), score0, score1, dice=dice)
else:
score1 = update(strategy1(score1, score0), score1, score0, dice=dice)
who = 1 - who
return score0, score1

update 是「规则函数」:simple_update(忽略 Sus Fuss)和 sus_update(加上 Sus Fuss)是同一套模拟器的两个插头。换规则不用改对局循环——这是「数据驱动 + 依赖注入」思想的第一次落地,也是 CS61A 反复强调的:把变化的部分变成参数。

策略:函数是一等公民#

玩家的策略就是 (我的分数, 对手分数) -> 掷骰数 的函数。always_roll(n) 用闭包返回「恒定掷 n 个骰」的策略:

def always_roll(n):
"""Return a player strategy that always rolls N dice."""
assert n >= 0 and n <= 10
def always(score0, score1):
return n
return always

Phase 3 的实验部分最有意思——用 蒙特卡洛 评估策略:make_averaged 包装任意函数并统计其 1000 次调用的平均值,从而回答「掷几个骰子期望收益最高」(max_scoring_num_rolls),再用 average_win_rate 让两个策略对打几千局比较胜率:

def make_averaged(original_function, samples_count=1000):
"""Return a function that returns the average value of ORIGINAL_FUNCTION
called SAMPLES_COUNT times."""
def averaged(*args):
total = 0
for _ in range(samples_count):
total += original_function(*args)
return total / samples_count
return averaged

(与提交版等价的简化写法)

TIP

make_averaged 就是一个「手动实现的装饰器」——返回一个包装原函数的新函数。学到后面看到 @ 语法时,会发现 Hog 项目里已经亲手写过它的原理。

二、Cats:打字测试与纠错#

Cats 是一个打字速度测试网站(带 GUI),核心是自动纠错:打错一个词时,从词库里选出「最接近」的正确词。

选择段落:高阶函数筛选#

pick(paragraphs, select, k)select 函数筛选段落,about(subject) 返回一个「判断段落是否包含主题词」的筛选函数——又一次把「条件」变成参数:

def about(subject):
"""Return a select function that returns whether
a paragraph contains one of the words in SUBJECT."""
def about_subject(para):
...
return about_subject

自动纠错:可插拔的差异度量#

autocorrect(typed_word, word_list, diff_function, limit) 是整套纠错的中枢:对词库里的每个词调用 diff_function 计算「差异」,取最小且不超过 limit 的词;若都超过 limit,返回原词(宁可不纠也不瞎纠)。

关键在于 diff_function 也是注入的——「怎样算接近」可以换成不同实现

  1. feline_fixes:只统计「替换」次数 + 长度差,递归实现:
def feline_fixes(typed, source, limit):
"""How many letters in TYPED need to be substituted to create SOURCE,
plus the difference in their lengths."""
if len(typed) == 0 or len(source) == 0:
return abs(len(typed) - len(source))
if typed[0] != source[0]:
return 1 + feline_fixes(typed[1:], source[1:], limit - 1)
return feline_fixes(typed[1:], source[1:], limit)

(与提交版等价的简化写法)

  1. minimum_mewtations:完整编辑距离(Levenshtein),支持插入/删除/替换三种操作——「纠错」真正变成了「求最小编辑路径」的算法问题。
WARNING

递归的坑:feline_fixes 忘了 limit 剪枝会怎样?typedsource 一直不相等时递归深度等于字符串长度,长词直接爆栈。每个递归分支都要朝「基准情形」收敛limit 不只是性能优化,还是正确性的一部分。

进度与多人模式#

report_progress 计算「已正确打到源文第几个词」的百分比并上报;多人对战部分(time_per_word / fastest_words)把每个玩家的按键时间戳换算成「每个词用时」,再统计谁在每个词上最快——纯数据变换练习,也顺便练了字典与列表的综合操作。

三、Ants:面向对象的塔防#

Ants 是「蚂蚁 vs 蜜蜂」塔防:蜜蜂沿着路径爬向蜂巢,蚂蚁在路径格子上阻挡并攻击。核心是一张精心设计的类继承图

Place(格子)← Water(水域:淹死不会潜水的蚂蚁)
Insect(血量/行动)← Bee(蜜蜂) / Ant(蚂蚁)
Ant ← HarvesterAnt(产食物)/ ThrowerAnt(掷叶攻击)
← ShortThrower / LongThrower(射程限制)
← FireAnt(死亡爆炸)/ WallAnt(高血量路障)
← HungryAnt(吞噬)/ ContainerAnt(可装其他蚂蚁)
← ScubaThrower / QueenAnt / NinjaAnt

多态:每回合做什么由子类决定#

每个 Insectaction(gamestate) 方法,游戏循环每回合对每个格子上的昆虫调用一次——父类不知道也不关心子类具体干什么HarvesterAnt 加食物、ThrowerAnt 找最近的蜜蜂掷叶、HungryAnt 吞掉路径上第一只蜜蜂。新增一种蚂蚁 = 新增一个子类 + 覆写 action,不动游戏循环。

覆写 reduce_health 的次序陷阱#

FireAnt 的死亡爆炸是覆写方法最经典的坑:必须先让父类扣血移除自己,再对同格的蜜蜂造成伤害——但父类移除后 self.place 就没了:

class FireAnt(Ant):
damage = 3
food_cost = 5
def reduce_health(self, amount):
list_bees = self.place.bees # 先保存引用
super().reduce_health(amount) # 再让父类扣血(可能移除自己)
total_damage = amount
if self.health <= 0:
total_damage += self.damage # 死亡时追加爆炸伤害
for bee in list(list_bees):
bee.reduce_health(total_damage)
IMPORTANT

覆写方法的顺序 = 状态机super() 调用前后 self 的状态可能完全不同(这里:调用后 self.place 可能已把自己移除)。凡是「先清理自己、再影响别人」的逻辑,都要先把需要的引用存下来。这个 bug 我第一次写反了,蚂蚁死了却没炸到任何蜜蜂。

类属性即配置#

每种蚂蚁用类属性声明 name / food_cost / damage / implemented,GUI 靠 ANT_CLASSES 注册表自动生成「选择哪种蚂蚁」的按钮——配置与行为分离,新蚂蚁写好类就自动出现在游戏里。ContainerAntBodyguardAnt / TankAnt)用「容器」模式让一只蚂蚁背上另一只,QueenAnt 每回合给后方所有蚂蚁 double() 伤害、自己阵亡则游戏失败——这些都是对「继承 + 覆写」语法的极限练习。

ants_plans.py 用数据(波次列表)定义 easy → extra_hard 五档难度,印证了项目一那句话:把变化的东西变成数据,把不变的东西变成函数

四、总结与思考#

三个项目连起来,正好是 CS61A 抽象观的递进:

  1. Hog:用函数抽象行为。 策略、规则、实验评估全是函数,「玩什么规则」只是 play 的一个参数。学会把流程参数化,是写出可复用代码的第一步。
  2. Cats:用数据 + 递归抽象问题。 纠错被拆成「可注入的差异度量」,编辑距离把模糊匹配变成可计算的算法。递归的基准情形与剪枝在这里有了真实的工程意义。
  3. Ants:用对象抽象世界。 一张继承图容纳了 15+ 种行为各异的单位,新增单位不动主循环。覆写顺序、super() 调用时机、类属性配置,是 OOP 的完整实战。

回头看,最值钱的不是记住了多少语法,而是「把变化的东西变成参数」这一条贯穿始终的原则——Hog 里的 update、Cats 里的 diff_function、Ants 里的类属性,都在反复训练同一件事:抽象出不变的结构,把变化留给调用者。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CS61A 三连:Hog、Cats 与 Ants——从函数式到面向对象
https://hajim1.art/posts/cs61a-projects-hog-cats-ants/
作者
Takamatsu Tomori
发布于
2025-04-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录