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, score1update 是「规则函数」: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 alwaysPhase 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 也是注入的——「怎样算接近」可以换成不同实现:
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)(与提交版等价的简化写法)
minimum_mewtations:完整编辑距离(Levenshtein),支持插入/删除/替换三种操作——「纠错」真正变成了「求最小编辑路径」的算法问题。
WARNING递归的坑:
feline_fixes忘了limit剪枝会怎样?typed和source一直不相等时递归深度等于字符串长度,长词直接爆栈。每个递归分支都要朝「基准情形」收敛,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多态:每回合做什么由子类决定
每个 Insect 有 action(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 注册表自动生成「选择哪种蚂蚁」的按钮——配置与行为分离,新蚂蚁写好类就自动出现在游戏里。ContainerAnt(BodyguardAnt / TankAnt)用「容器」模式让一只蚂蚁背上另一只,QueenAnt 每回合给后方所有蚂蚁 double() 伤害、自己阵亡则游戏失败——这些都是对「继承 + 覆写」语法的极限练习。
ants_plans.py 用数据(波次列表)定义 easy → extra_hard 五档难度,印证了项目一那句话:把变化的东西变成数据,把不变的东西变成函数。
四、总结与思考
三个项目连起来,正好是 CS61A 抽象观的递进:
- Hog:用函数抽象行为。 策略、规则、实验评估全是函数,「玩什么规则」只是
play的一个参数。学会把流程参数化,是写出可复用代码的第一步。 - Cats:用数据 + 递归抽象问题。 纠错被拆成「可注入的差异度量」,编辑距离把模糊匹配变成可计算的算法。递归的基准情形与剪枝在这里有了真实的工程意义。
- Ants:用对象抽象世界。 一张继承图容纳了 15+ 种行为各异的单位,新增单位不动主循环。覆写顺序、
super()调用时机、类属性配置,是 OOP 的完整实战。
回头看,最值钱的不是记住了多少语法,而是「把变化的东西变成参数」这一条贯穿始终的原则——Hog 里的
update、Cats 里的diff_function、Ants 里的类属性,都在反复训练同一件事:抽象出不变的结构,把变化留给调用者。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时





