项目 2:CS 61A 自动纠错打字软件

程序员的梦想是
抽象、递归,以及
快速打字。
介绍
重要提交事项: 为了获得完整学分:
- 在2月22日星期四之前提交完成的第1阶段和第2阶段,占1分。
- 在2月27日星期二之前提交完成的所有阶段。
建议按顺序完成各个问题。因为后面的问题在实现上依赖于前面的问题,所以也依赖于
ok测试的运行结果。你可以和你的伙伴一起完成整个项目。
您可以在2月26日星期一之前提交整个项目,就能获得1个奖励积分。
在这个项目中,您将编写一个程序来测量打字速度。此外,您将实现打字自动纠错功能,该功能尝试在用户键入一个单词后纠正该单词的拼写。这个项目的灵感来源于 typeracer。
最终产品
你可以在 cats.cs61a.org 上体验我们的项目示例。欢迎随时试用。完成这个项目后,你就能亲手实现它的很多重要功能了!
下载起始文件
你可以下载 zip 压缩包 来获取所有项目代码。这个项目包含多个文件,你只需要修改 cats.py。压缩包中包含以下文件:
cats.py:打字测试逻辑。utils.py:用于文件和字符串操作的实用函数。ucb.py:CS 61A 项目的实用函数。data/sample_paragraphs.txt:用于打字练习的文本示例。这些文本示例来源于维基百科上关于各种主题的文章,是通过抓取获得的。data/common_words.txt:按频率排序的常见 英语单词。data/words.txt:更多按频率排序的 英语单词。data/final_diff_words.txt:更多英语单词!data/testcases.out:Final Diff 扩展(可选)的测试用例。cats_gui.py:基于 Web 的图形用户界面 (GUI) 服务器。gui_files:图形用户界面 (GUI) 相关文件目录。multiplayer:支持多人模式的相关文件目录。favicons:图标目录。images:图像目录。ok、proj02.ok、tests:测试文件。score.py:可选的 Final Diff 扩展的一部分。
后勤
这个项目总分20分,其中19分考察代码的正确性,1分奖励在检查点日期前提交第一阶段和第二阶段的同学。
你只需要修改并提交 cats.py 文件即可完成项目。要提交项目,请将 cats.py 文件提交到 Gradescope 上对应的作业。
对于需要你实现的函数,我们提供了一些初始代码。你可以选择直接使用,也可以删除后从头开始编写。你也可以根据需要添加新的函数定义。
但是,请勿修改上述未列出的任何其他函数或文件。 否则,您的代码可能无法通过自动评分器的测试。 此外,请勿更改任何函数签名(包括名称、参数顺序或参数数量)。
在整个项目中,您应该经常测试代码的正确性。 这是一个好习惯,有助于快速定位问题。 但也不应过于频繁地测试,给自己留出思考的时间。
我们提供了一个名为 ok 的自动评分器,以帮助您测试代码并跟踪进度。 首次运行自动评分器时,系统会提示您通过 Web 浏览器登录您的 Ok 帐户。 请按照提示操作。 每次运行 ok,它都会在我们的服务器上备份您的工作和进度。
ok 的主要作用是测试您的代码实现。
如果您想以交互模式测试您的代码,可以运行
python3 ok -q [问题编号] -i
并填入相应的问题编号(例如 01)。 这将运行该问题的测试,直到遇到第一个失败的测试为止,然后您就可以交互式地测试您编写的函数了。
您还可以通过编写以下代码,使用 OK 的调试打印功能:
print("DEBUG:", x)
这样会在终端中生成调试信息,而不会因为额外输出导致 OK 测试失败。
入门视频
观看这些视频需要您登录 berkeley.edu 邮箱。
第 1 阶段:打字
问题 1 (1 分)
在整个项目中,我们将对 cats.py 中的函数进行更改。
实现 pick 函数。 此函数用于选择用户将要输入的段落。 它接受三个参数:
- 一个字符串列表,其中每个字符串代表一个段落,称为
paragraphs - 一个
select函数,它为可以被选择的段落返回True - 一个非负索引
k
pick 函数返回 select 返回 True 的第 k 个段落。 如果不存在这样的段落(因为 k 太大),则 pick 返回空字符串。
解锁完成后,开始编写代码实现您的解决方案。 您可以使用以下命令检查代码的正确性:
python3 ok -q 01 -u
您可以使用以下命令检查代码的正确性:
python3 ok -q 01
问题 2 (1 分)
实现 about 函数。 该函数接受一个 subject 单词列表,并返回一个函数。 返回的函数接受一个段落作为输入,并返回一个布尔值,指示该段落是否包含 subject 列表中的任何单词。
实现 about 函数后,我们就可以将返回的函数作为 select 参数传递给 pick 函数,这有助于我们继续完成打字测试的实现。
为了准确进行比较,您需要忽略大小写(即,将大写和小写字母视为相同)以及段落中的标点符号。 此外,只检查 subject 列表中单词的完全匹配,而不是子字符串匹配。 例如,“dogs”与“dog”不匹配。
提示:使用
utils.py中的split、lower和remove_punctuation函数。
解锁完成后,开始编写代码实现您的解决方案。 您可以使用以下命令检查代码的正确性:
python3 ok -q 02 -u
您可以使用以下命令检查代码的正确性:
python3 ok -q 02
问题 3 (2 分)
实现 accuracy 函数,该函数接受一个 typed 段落和一个 source 段落。它返回 typed 中与 source 中对应位置的单词完全匹配的百分比。大小写和标点符号也必须完全一致。“相应”指的是typed和source中相同位置的单词需要完全匹配,即第一个单词和第一个单词对应,第二个单词和第二个单词对应,以此类推。
在这里,“单词”指的是由空格分隔的连续字符序列,例如,"dog;" 会被当作一个单词。
如果 typed 比 source 长,那么多出的、在 source 中没有对应项的单词都被认为是错误的。
如果 typed 为空而 source 不为空,或者反之,准确率都为零。
在编写任何代码之前,请解锁测试以验证您对问题的理解:
python3 ok -q 03 -u
解锁完成后,即可开始编写代码。您可以使用以下命令检查您的正确性:
python3 ok -q 03
问题 4 (1 分)
实现 wpm 函数,该函数计算每分钟字数,这是一种衡量打字速度的指标,给定一个字符串 typed 和经过的 elapsed 时间(以秒为单位)。 虽然名为“每分钟字数”,但它并非基于实际键入的单词数量,而是以每 5 个字符为一组进行计算,这样可以避免因单词长度不同而造成的测试偏差。每分钟字数 的公式是将键入的字符数(包括空格)除以 5(典型单词长度)与经过的时间(以分钟为单位)的比率。
例如,字符串 "I am glad!" 包含十个字符(不包括引号)。每分钟字数的计算使用 2 作为键入的字数(因为 10 / 5 = 2)。如果有人在 30 秒(半分钟)内键入此字符串,则他们的速度将为每分钟 4 个字。
在编写任何代码之前,请解锁测试以验证您对问题的理解:
python3 ok -q 04 -u
解锁完成后,即可开始编写代码。您可以使用以下命令检查您的正确性:
python3 ok -q 04
是时候测试您的打字速度了! 您可以使用命令行来测试您在关于特定主题的段落上的打字速度。例如,以下命令将加载关于猫或小猫的段落。如果您好奇,请参阅 run_typing_test 函数的实现(但它是为您定义的)。
python3 cats.py -t cats kittens
您可以使用以下命令体验网页版图形界面(GUI)。关闭浏览器标签页后,您可能需要在终端使用 Ctrl+C 或 Cmd+C 退出GUI。
python3 cats_gui.py
第 2 阶段:自动更正
在基于 Web 的 GUI 中,有一个自动更正按钮,但现在它没有任何作用。我们将实现自动纠正拼写错误的功能。当用户按下空格键时,如果最后一个单词不在字典中,但存在与之相近的单词,程序会自动用该相近单词替换用户输入。
问题 5 (2 分)
实现 autocorrect 函数,该函数接受 typed_word、word_list、diff_function 和 limit。autocorrect 的目标是返回 word_list 中与提供的 typed_word 最接近的单词。
具体来说,autocorrect 执行以下操作:
- 如果
typed_word存在于word_list中,autocorrect则返回该单词。 - 否则,
autocorrect根据diff_function返回word_list中与typed_word差异最小的单词。 - 但是,如果
typed_word与word_list中任何单词的最小差异都大于limit,则返回typed_word。也就是说,limit限制了可以被纠正的拼写错误的程度。
注意:假设 typed_word 和 word_list 的所有元素都是小写的,并且没有标点符号。
重要提示:如果
word_list中的多个字符串与typed_word的差异程度相同,则autocorrect应返回在word_list中最靠前的字符串。
diff 函数 (diff function) 接受三个参数。第一个是 typed_word,第二个是源单词(在本例中,是 word_list 中的一个单词),第三个参数是 limit。diff 函数的输出是一个数字,表示两个字符串之间的差异量。
以下是一个 diff 函数的示例,该函数计算 1 + limit 和两个输入字符串长度之差的最小值:
>>> def length_diff(w1, w2, limit):
... return min(limit + 1, abs(len(w2) - len(w1)))
>>> length_diff('mellow', 'cello', 10)
1
>>> length_diff('hippo', 'hippopotamus', 5)
6
提示:尝试将
max或min与可选的key参数(接受单参数函数)一起使用。例如,max([-7, 2, -1], key = abs)将返回-7,因为abs(-7)大于abs(2)和abs(-1)。
在编写任何代码之前,请解锁测试以验证您对问题的理解:
python3 ok -q 05 -u
完成解锁后,开始实施您的解决方案。您可以使用以下命令检查您的正确性:
python3 ok -q 05
问题 6 (3 分)
实现 feline_fixes,它是一个接受两个字符串的 diff 函数。它返回将 typed 单词转换为 source 单词所需更改的最小字符数。如果字符串的长度不相等,则将长度差添加到总数中。
以下是一些例子:
>>> big_limit = 10
>>> feline_fixes("nice", "rice", big_limit) # Substitute: n -> r
1
>>> feline_fixes("range", "rungs", big_limit) # Substitute: a -> u, e -> s
2
>>> feline_fixes("pill", "pillage", big_limit) # Don't substitute anything, length difference of 3.
3
>>> feline_fixes("goodbye", "good", big_limit) # Don't substitute anything, length difference of 3.
3
>>> feline_fixes("roses", "arose", big_limit) # Substitute: r -> a, o -> r, s -> o, e -> s, s -> e
5
>>> feline_fixes("rose", "hello", big_limit) # Substitute: r->h, o->e, s->l, e->l, length difference of 1.
5
重要提示:您不得在实现中使用
while、for或列表推导式。请使用递归。
如果必须更改的字符数大于 limit,则 feline_fixes 应返回任何大于 limit 的数字,并应在超出 limit 后,尽量减少额外的计算量。
为什么要有这个限制?我们知道,如果
typed_word与word_list中任何单词的差异大于limit,autocorrect将会拒绝更正typed_word。差异超过limit1 还是 100,结果都一样;autocorrect都会拒绝更正。因此,一旦确定差异会超过limit,即使返回的结果不完全精确,也应该尽量减少不必要的计算。
以下两个对
feline_fixes的调用应该花费大致相同的时间来评估:>>> limit = 4
>>> feline_fixes("roses", "arose", limit) > limit
True
>>> feline_fixes("rosesabcdefghijklm", "arosenopqrstuvwxyz", limit) > limit
True
为了确保您正确地最小化达到 limit 后执行的额外计算量,我们有一个自动评分器测试,它根据函数调用的次数来衡量您解决方案的性能。这个测试并非完美;即使你成功避免了额外的计算,使用辅助函数也可能导致测试失败。
在编写任何代码之前,请解锁测试以验证你对问题的理解:
python3 ok -q 06 -u
解锁完成后,开始编写你的代码。您可以使用以下命令检查你的代码是否正确:
python3 ok -q 06
尝试在 GUI 中打开自动更正。它能帮助你提高打字速度吗?更正是否准确?
问题 7 (3 分)
实现 minimum_mewtations,这是一个差异函数,它返回将 typed 单词转换为 source 单词所需的最少编辑操作步骤。
有三种编辑操作步骤,以下是一些示例:
向
typed添加一个字母。- 向
"itten"添加"k"得到"kitten"。
- 向
从
typed中删除一个字母。- 从
"scat"中删除"s"得到"cat"。
- 从
将
typed中的一个字母替换为另一个字母。- 将
"zaguar"中的"z"替换为"j"得到"jaguar"。
- 将
每个编辑操作步骤都会使两个单词之间的差异增加 1。
>>> big_limit = 10
>>> minimum_mewtations("cats", "scat", big_limit) # cats -> scats -> scat
2
>>> minimum_mewtations("purng", "purring", big_limit) # purng -> purrng -> purring
2
>>> minimum_mewtations("ckiteus", "kittens", big_limit) # ckiteus -> kiteus -> kitteus -> kittens
3
我们已经在cats.py中提供了一个代码模板。你可以修改或删除它,从头开始编写。
提示: 这是一个具有三个递归调用的递归函数。其中一个递归调用将类似于
feline_fixes中的递归调用。此外,您需要多个基本情况才能解决此问题。
如果所需的编辑次数大于 limit,则 minimum_mewtations 应返回任何大于 limit 的数字,并应尽量减少执行此操作所需的计算量。
以下两个对
minimum_mewtations的调用应该花费大致相同的时间来评估:>>> limit = 2
>>> minimum_mewtations("ckiteus", "kittens", limit) > limit
True
>>> minimum_mewtations("ckiteusabcdefghijklm", "kittensnopqrstuvwxyz", limit) > limit
True
为了确保您正确地最小化达到 limit 后执行的额外计算量,我们有一个自动评分器测试,它根据函数调用的次数来衡量您解决方案的性能。
在编写任何代码之前,请解锁测试以验证您对问题的理解:
python3 ok -q 07 -u
解锁完成后,开始编写你的代码。你可以使用以下命令来检查你的代码是否正确:
python3 ok -q 07
再试一次打字。看看自动更正有没有更准确?
python3 cats_gui.py
提交你的第一阶段和第二阶段A的检查点
请检查你是否完成了第一阶段和第二阶段A的所有问题:
python3 ok --score
然后,在检查点截止日期之前,把cats.py提交到Gradescope上名为 Cats Checkpoint 的作业。
如果你完成了到目前为止的所有题目,就能拿到检查点的全部学分。
(可选)扩展:最终差异 (0 分)
你可以选择设计你自己的差异函数,称为 final_diff。这里有一些让自动更正更准确的建议:
- 考虑一下哪些添加或删除操作更有可能发生。例如,如果你不小心遗漏了一个字母,如果它连续出现两次,则更有可能。
- 如果两个相邻字母的位置颠倒了,算作一次修改,而不是两次。
- 尝试合并常见的拼写错误。
你还可以通过更改 cats.py 中变量 FINAL_DIFF_LIMIT 的值来设置你希望差异函数使用的限制。
您可以通过运行以下命令来检查 final_diff 的成功率:
python3 score.py
如果你不知道从哪里开始,请尝试将 feline_fixes 和 minimum_mewtations 的代码复制粘贴到 final_diff 中并对其进行评分。看看它无意中修正的拼写错误,也许能给你一些启发!
第 3 阶段:多人游戏
和朋友联机比赛打字更有趣!接下来,你要实现多人游戏功能。这样,当你在电脑上运行cats_gui.py的时候,它就会连接到cats.cs61a.org的服务器,寻找其他玩家一起比赛。
要和朋友联机比赛,需要同时运行五个程序:
- 你的GUI,负责处理网页浏览器里的文字颜色和显示。
- 你的
cats_gui.py,是一个Web服务器,它使用你在cats.py里写的代码和你的GUI进行通讯。 - 你的对手的
cats_gui.py。 - 你的对手的 GUI。
- CS 61A 多人游戏服务器,它将玩家匹配在一起并传递消息。
当您键入时,您的 GUI 会将您键入的内容上传到您的 cats_gui.py 服务器,该服务器会计算您已取得的进展并返回进度更新。它还会将进度更新上传到多人游戏服务器,以便您对手的 GUI 可以显示它。
同时,您的 GUI 显示始终尝试通过从 cats_gui.py 请求进度更新来保持最新,而 cats_gui.py 又从多人游戏服务器请求该信息。
每个玩家都有一个 id 号,服务器使用该号码来跟踪打字进度。
问题 8(2 分)
实现 report_progress,每次用户完成键入一个单词时都会调用它。这个函数接收你输入的单词列表typed,原文的单词列表source,用户的user_id,以及一个upload函数,用来把进度报告上传到多人游戏服务器。typed 中的单词永远不会多于 source 中的单词。
你的进度是这样计算的:在source中,你正确输入的单词数量,直到第一个错误单词为止,然后用这个数量除以source中单词的总数。例如,在这个例子里,进度是0.25:
report_progress(["Hello", "ths", "is"], ["Hello", "this", "is", "wrong"], ...)
你的 report_progress 函数应该做两件事:向多人服务器上传消息,并返回 user_id 对应的玩家的进度。
你可以通过调用 upload 函数,并传入一个包含键 'id' 和 'progress' 的字典,来向多人服务器上传消息。然后你应该返回玩家的进度,也就是你计算出的正确单词比例。
提示: 请参阅下面的字典,了解
upload函数的潜在输入示例。此字典表示user_id为 1 且progress为 0.6 的玩家。
{'id': 1, 'progress': 0.6}
在编写任何代码之前,请解锁测试以验证你对问题的理解:
python3 ok -q 08 -u
解锁完成后,就可以开始编写你的解决方案了。你可以使用以下命令检查你的正确性:
python3 ok -q 08
问题 9 (2 分值)
实现 time_per_word,它接受一个列表 words 和 timestamps_per_player,这是一个列表的列表,其中包含每个玩家的时间戳,指示每个玩家何时完成输入 words 中的每个单词。它返回一个包含了这些信息的 match 对象。
match 是一个 数据抽象,它表示多个玩家之间的打字“比赛”。具体来说,每个 match 对象都存储了实例变量 words 和 times。
times是一个列表的列表,存储了每个玩家输入每个单词所用的时间。- 具体来说,
times[i][j]表示玩家i输入words[j]所花费的时间。
例如,假设 words = ['Hello', 'world'] 并且 times = [[5, 1], [4, 2]],那么 [5, 1] 对应于玩家 0 的时间列表,而 [4, 2] 对应于玩家 1 的时间列表。因此,玩家 0 花了 5 个时间单位输入单词 'Hello'。
重要提示: 在返回
match对象时,请确保使用match构造函数。测试会检查你是否使用了match数据抽象,而不是直接使用特定的数据格式。有关更多信息,你可以阅读下面或
cats.py中match构造函数的定义。但是,与任何数据抽象一样,我们更关心函数的功能,而不是具体的实现方式。
def match(words, times):
"""A data abstraction containing all words typed and their times.
Arguments:
words: A list of strings, each string representing a word typed.
times: A list of lists for how long it took for each player to type
each word.
times[i][j] = time it took for player i to type words[j].
Example input:
words: ['Hello', 'world']
times: [[5, 1], [4, 2]]
"""
assert all([type(w) == str for w in words]), 'words should be a list of strings'
assert all([type(t) == list for t in times]), 'times should be a list of lists'
assert all([isinstance(i, (int, float)) for t in times for i in t]), 'times lists should contain numbers'
assert all([len(t) == len(words) for t in times]), 'There should be one word per time.'
return {"words": words, "times": times}
def get_word(match, word_index):
"""A utility function that gets the word with index word_index"""
assert 0 <= word_index < len(get_all_words(match)), "word_index out of range of words"
return get_all_words(match)[word_index]
def time(match, player_num, word_index):
"""A utility function for the time it took player_num to type the word at word_index"""
assert word_index < len(get_all_words(match)), "word_index out of range of words"
assert player_num < len(get_all_times(match)), "player_num out of range of players"
return get_all_times(match)[player_num][word_index]
def get_all_words(match):
"""A selector function for all the words in the match"""
return match["words"]
def get_all_times(match):
"""A selector function for all typing times for all players"""
return match["times"]
def match_string(match):
"""A helper function that takes in a match data abstraction and returns a string representation of it"""
return f"match({get_all_words(match)}, {get_all_times(match)})"
时间戳是累积的,并且总是递增的。times 列表中的数值代表每个玩家连续时间戳之间的差值。
举例来说,如果 timestamps_per_player = [[1, 3, 5], [2, 5, 6]],那么 match 对象中对应的 times 属性将会是 [[2, 2], [3, 1]]。这是因为第一个玩家的时间戳差值分别是 (3-1) 和 (5-3),而第二个玩家的时间戳差值分别是 (5-2) 和 (6-5)。timestamps_per_player 中每个列表的第一个值表示每个玩家的初始开始时间。
解锁测试后,开始编写你的代码。你可以使用以下命令来检查代码的正确性:
python3 ok -q 09 -u
完成解锁后,开始实施您的解决方案。您可以使用以下命令检查您的正确性:
python3 ok -q 09
问题 10 (2 分)
实现 fastest_words 函数,该函数返回每个玩家打字速度最快的单词。此函数在所有玩家完成输入后调用。它接受一个 match。
更具体地说,fastest_words 函数返回一个列表,其中包含多个单词列表。每个子列表对应一个玩家,并包含该玩家打字速度最快的单词(与其他玩家相比)。如果出现平局,则认为在列表中排名最靠前的玩家(即玩家索引最小的玩家)打字速度最快。
例如,考虑以下包含单词 'Just'、'have' 和 'fun' 的比赛。玩家 0 输入 'fun' 最快(3 秒),玩家 1 输入 'Just' 最快(4 秒),他们在单词 'have' 上打成平手(都花了 1 秒),因此我们认为玩家 0 是最快的,因为他们是列表中最早的玩家。
>>> player_0 = [5, 1, 3]
>>> player_1 = [4, 1, 6]
>>> fastest_words(match(['Just', 'have', 'fun'], [player_0, player_1]))
[['have', 'fun'], ['Just']]
match 参数是一个 match 数据抽象,与我们在问题 9 中返回的类型相同。
你可以使用 get_word 选择器来访问 match 对象中的单词。该选择器接受一个 match 对象和一个 word_index(整数)作为参数。
此外,你可以使用 time 函数来获取玩家在特定索引位置输入某个单词所花费的时间。除了 match 对象和 word_index 之外,该函数还需要一个整数类型的 player_num 参数。
通过这两个函数和一个 match 对象,我们可以轻松地获取任何玩家输入任何单词所花费的时间!
>>> player_0 = [5, 1, 3]
>>> player_1 = [4, 1, 6]
>>> ex_match = match(['Just', 'have', 'fun'], [player_0, player_1])
>>> get_word(ex_match, 2)
'fun'
>>> time(ex_match, 0, 2)
3
重要提醒:使用
match数据结构时,请务必使用其对应的选择器。测试会检查你是否正确使用了match数据抽象,而不是直接使用某种特定的数据格式。确保你的实现不会改变给定的玩家输入列表。对于上面的示例,在
[player_0, player_1]上调用fastest_words不应改变player_0或player_1。
在编写任何代码之前,请解锁测试以验证你对问题的理解:
python3 ok -q 10 -u
完成解锁后,开始编写你的代码。你可以使用以下命令来检查你的代码是否正确:
python3 ok -q 10
恭喜!现在你可以与课程中的其他学生对战了。在 cats.py 底部附近将 enable_multiplayer 设置为 True,然后快速打字!
python3 cats_gui.py
项目提交
运行 ok 以检查所有问题,确保所有测试都已解锁并通过:
python3 ok
你还可以检查项目每个部分的得分:
python3 ok --score
满意后,通过将 cats.py 上传到 Gradescope 上的 Cats 作业来提交此作业。有关如何执行此操作的复习,请参阅 Lab 00。
您可以通过单击提交内容右侧姓名下的 + Add Group Member 将合作伙伴添加到您的 Gradescope 提交内容中。只需要一个合作伙伴提交到 Gradescope。