Wordle 详细功能要求¶
本页从往届 Wordle 大作业文档整理而来,只保留本届 Lab 4 要实现的八组基础功能。
本页可以在简洁版和详细版之间切换:
- 简洁版本去掉了提示,只保留作业要求
- 详细版本包括所有内容
刚开始时建议选择详细版;熟悉内容后,可以切换到简洁版检查自己的实现。
基础功能要求¶
后面的功能会用到前面已经实现的内容,因此建议按顺序完成。正式评分时,下面八组功能测试分别计分。
八组基础功能使用 Rust 的测试框架进行自动测试。为了便于测试,根据标准输出是否为交互式终端(项目模板中提供了 is_tty 用于判断),你的程序必须实现两种输出模式:
- 如果是,则使用较为友好和直观的输出(称为交互模式)。
- 如果不是,则严格遵守下面定义的输出格式(称为测试模式)。如果有任何不可恢复的错误(如参数格式错误、文件不存在等),程序必须以非正常返回值退出。
在项目根目录运行 cargo test -- --test-threads=1 可以检查基础功能。
关于测试的注意事项
- 公开测试只覆盖部分输入;正式评分还会使用助教保留的测试,因此仍需完整实现下面的要求。
- 每个测试点均有运行时间限制,超时将会直接认为失败。
如无特别说明,下面所有涉及到 Wordle 游戏中字母的打印都必须全部大写。
- (0.25 分)程序启动时,从标准输入读取一个词作为答案,开始一局游戏(一局游戏最多 6 次猜测)。候选词与可用词内嵌在程序中(分别对应代码
src/builtin_words.rs中的FINAL和ACCEPTABLE)。从标准输入读取一行用户的猜测词:- 在每次猜测(获得可用词列表中的输入)后打印猜测结果以及所有字母的状态:
- 测试模式:程序启动后,不进行任何输出,等待用户的输入。在测试模式下可以认为输入的答案词符合要求。每次猜测后,如果输入符合要求,则打印一行,形如
SSSSS AAAAAAAAAAAAAAAAAAAAAAAAAA,前面五个字母是用户的猜测结果,后面 26 个字母是所有字母的状态(类似 Wordle 游戏中的输入键盘)。S和A允许的取值包括R(Red,数量过多的字母)、Y(Yellow,位置不正确的字母)、G(Green,正确的字母)、X(Unknown,未知状态的字母),语义与作业背景中描述的一致。如果读入的一行不符合要求,则打印INVALID,重新猜测,不消耗猜测次数。在打印所有字母状态时,如果某个字母在猜测中有多个不同的状态,则选择最「好」的一种,即优先级为G>Y>R>X。 - 交互模式:每次猜测后向标准输出打印到本次为止的所有猜测结果,以及所有字母的状态(模拟 Wordle 网页的状态)。必须使用带颜色的输出,
R用红色,Y用黄色,G用绿色,X不需要设置颜色。
- 测试模式:程序启动后,不进行任何输出,等待用户的输入。在测试模式下可以认为输入的答案词符合要求。每次猜测后,如果输入符合要求,则打印一行,形如
- 在游戏结束(用完六次猜测机会或者猜对)后打印猜测次数(包括猜对的最后一次),如果失败,则还需打印正确答案:
- 测试模式:打印
CORRECT n(其中n为猜测次数)/FAILED ZZZZZ(其中ZZZZZ为答案)
- 测试模式:打印
- 游戏结束后正常退出程序
- 在每次猜测(获得可用词列表中的输入)后打印猜测结果以及所有字母的状态:
-
(0.25 分)记上面从标准输入指定答案的模式为指定答案模式。在指定答案模式下,增加命令行参数
-w/--word用于指定答案,如-w build或者--word build表示指定答案为build,不再需要从标准输入读入答案;如果不指定,则依旧从标准输入读入答案。增加命令行参数-r/--random用于启动随机模式,在随机模式下,不再采用指定答案模式,而是从候选词库中(项目模版中给定)随机抽取问题。随机模式和指定答案模式只能二选一,默认情况下是指定答案模式。程序在测试模式下的输出格式不变。解析命令行参数的方法
解析命令行参数有两种方法:
-
遍历
std::env::args()返回的迭代器,逐个解析命令行参数,当遇到-w/--word参数的时候,记录状态,把遇到的下一个命令行参数保存下来;当遇到-r/--random参数的时候,启动随机模式。伪代码:meet_word_argument = 0 random_mode = 0 for each command line argument if meet_word_argument is 1 word_argument = current_argument meet_word_argument = 0 else if current_argument is -w or --word meet_word_argument = 1 else if current_argument is -r or --random random_mode = 1 end if end for if meet_word_argument is 1 error! missing word argument end if -
自学第三方库 clap 的使用:阅读 Clap Derive Tutorial 或者 Clap Builder Tutorial,两种写法二选一;推荐阅读 Command Line Applications in Rust
-
-
(0.25 分)增加命令行参数
-D/--difficult表示启动困难模式。在此模式下,新的猜测中所有已知位置正确(绿色,即G)的字母不能改变位置,也必须用到所有存在但位置不正确(黄色,即Y)的字母,但是允许在新的猜测中重复使用数量过多(红色,即R)的字母,也允许存在但位置不正确(黄色,即Y)的字母再次出现在相同的错误位置。此选项不改变程序在测试模式下的输出格式。 -
(0.25 分)如果没有使用
-w/--word参数指定答案,则每局游戏结束后开始询问是否继续下一局。如果是随机模式,则在用户退出前,要求随机的答案不能与已经玩过的单词重复;如果是指定答案模式(且没有使用-w/--word通过命令行参数指定答案),每局重新从标准输入读入一个新的答案词。增加命令行参数-t/--stats表示在每局后,统计并输出截至目前的游戏成功率(成功局数 / 已玩局数)、平均尝试次数(仅计算成功的游戏,如果没有成功的游戏则为0)、所有猜测中(按次数降序和字典序升序排序)最频繁使用的五个词和次数。- 测试模式:
- 每局结束后,如果指定
-t/--stats则额外进行以下操作:- 打印一行
X Y Z,分别为成功局数、失败局数(均为整数)和成功游戏的平均尝试次数(浮点数,四舍五入保留到小数点后两位); - 打印一行
W_1 i_1 ... W_5 i_5,分别是最频繁使用的五个词和次数;如果不足五个,则有多少输出多少;
- 打印一行
- 每局结束后,如果没有用
-w/--word参数指定答案,则询问是否继续下一局:读入一行,如果为Y则继续,如果是N或者 EOF 标志(read_line返回Ok(0))则退出; - 每局结束后,如果用
-w/--word指定了答案,则退出
- 每局结束后,如果指定
- 交互模式:以自定义方式显示信息
多关键字排序
多关键字排序(这里涉及到两个关键字,分别是猜测次数和猜测词)有两种实现方法:
- 使用稳定排序算法,先排序一个关键字,再排序另一个关键字。想想应该先排哪一个,后排哪一个?为什么需要稳定排序算法?
- 自定义一个比较函数,比较的时候,把两个关键字都考虑进来,然后一次性排好序
- 测试模式:
-
(0.25 分)在随机模式中,增加命令行参数
-d/--day用于指定开始时的局数(如-d 5表示跳过前四局,从第五局开始;默认值为 1,且不能超过答案词库的大小A),-s/--seed用于指定随机种子(类型是u64,可选,默认为一个自选的确定值)。在候选词库不变的情况下,游戏应该被这两个参数唯一确定。随机模式下不允许使用-w/--word参数,指定答案模式下不允许使用-d/--day和-s/--seed参数,如果出现了冲突,则报告错误并以非正常返回值退出。这些选项不改变程序在测试模式下的输出格式。形式上来说,需要构造一个函数 \(w = \text{ans}[f(d,s)]\) 用来确定答案 \(w\) 在答案词库中的下标,满足:- 对于任何固定的 \(s\),\(f(1,s)\dots f(A,s)\) 应该恰好是 \(\{1 \dots A\}\) 的一个无重复排列
- 对于任意固定的 \(d\),\(f\) 不允许是恒等变换(即不同的种子必须对应不同的排列)
- 为了方便检查,规定在实现中,必须使用
randcrate(版本为0.8.5)提供的shuffle方法,随机数引擎必须使用rand::rngs::StdRng,并把s作为种子传入。具体来说,你需要先用s作为种子初始化一个rand::rngs::StdRng(使用seed_from_u64(u64)),再用这个StdRng去shuffle候选词库,最后以day - 1作为下标访问打乱后的数组。
数组打乱的方法
为了保证多次随机出来的数不重复,把所有可能的值放在一个数组中,随机打乱以后,按顺序取出来,这样做既保证了随机性,又保证了不重复性。
shuffle函数实现了 Fisher-Yates 算法,这是一个很经典的数组打乱算法,值得学习。 -
(0.25 分)增加命令行参数
-f/--final-set以及-a/--acceptable-set用于指定候选词库和可用词库文件(如-f final.txt -a acceptable.txt)。如果不指定,则使用内置的词库。文件的格式均为按行分割的单词列表(不区分大小写)。加载时需要检查是否符合格式要求、是否存在重复,并且候选词库必须严格是可用词库的子集。如果在指定答案模式中,则也要检查用户给定的答案是否在候选词库中。在读入词库后,需要将其全部转为大写,并按字典序排序。提示
读取文件内容,可以用 Rust 标准库中的函数,如:
std::fs::File::open:打开一个文件std::io::Read::read_to_string:从已经打开的文件里,读取整个文件的内容到Stringstd::fs::read:跳过打开文件的步骤,直接从文件读取数据到Vec<u8>std::fs::read_to_string:跳过打开文件的步骤,直接从文件读取数据为String
检查是否存在重复的元素,以及判断两个集合之间是否为子集的关系,可以用 Rust 中对应集合的数据结构来实现。
-
(0.25 分)增加命令行参数
-S/--state用于保存和加载随机模式的游戏状态(如-S state.json),格式为如下的 JSON 文件:
{
"total_rounds": 1,
"games": [
{
"answer": "PROXY",
"guesses": ["CRANE", "PROUD", "PROXY"]
}
]
}
total_rounds 表示已经玩过的总局数,games 保存所有对局;每局记录答案 answer 和猜测历史 guesses。
提示
向文件写入内容,可以用 Rust 标准库中的函数,如:
std::fs::File::create:以写模式打开或新建一个文件std::io::Write::write:向已经打开的文件写入数据(&[u8])std::fs::write:跳过打开文件的步骤,直接向文件写入数据
JSON 是一种数据交换格式,支持数组、字典(Map)、字符串、数字、bool 和 null。具体的表达方式如下:
- 数组:以
[开头,接着是以,分隔的数组元素,最后以]结尾。元素可以是任意类型。 - 字典(Map):以
{开头,接着是若干个键(Key)值(Value)对最后以}结尾。键值对之间用,分隔,键和值之间用:分隔。键必须是字符串,值可以是任意类型。 - 字符串:用两个
"括起来的字符串,支持转义字符。 - 数字:十进制数,支持小数。
- bool:true 或者 false。
- null
读写 JSON 有两种方法:
- 自行解析 JSON,例如:找到第一个
{字符,然后找到前两次出现的"字符,提取出双引号之间的字符串,判断它是total_rounds还是games;如果是total_rounds,找到:和,的位置,提取出中间的数字;如果是games,那么找到[,再找到{,重复类似前面的过程,分别解析出answer和guesses的内容。最后把数据放在一个自定义的 struct 中。注意字典里面键(Key)的顺序是不固定的,不保证total_round一定出现在games之前。 - 使用 serde_json 或 json 库来操作 JSON。推荐阅读 How to Work With JSON in Rust 和 Rust - Serde Json By Example。
在游戏启动时,如果状态文件存在,则加载此前的状态,不存在则忽略。文件必须是合法 JSON,字段类型必须正确,并且不能包含未定义的字段;字段可以缺少,缺少时使用默认值。无需检查其中的单词是否属于本次使用的词库,也无需检查局数和对局记录之间的关系。格式不符合要求时,程序应当报告错误并以非正常返回值退出。
提示
这样设计,一方面防止错误地加载了来自其他软件的 JSON,如果出现了不认识的键值,那大概率说明加载错了 JSON。另一方面,考虑到兼容性,为了读取旧版本程序生成的状态文件,此时状态文件可能会缺少一些新版本才有的键值,所以缺少键值是允许的。
每次结束一局游戏后,需要将当前状态写入文件中。注意在上面打印的游戏统计中,需要包含此前所有的游戏,而非仅本次启动后的;每次启动均视为新的一局,即 total_rounds 不影响 day 的效果,仅用于计算统计数据。
提示
这其实就是游戏的实时存档功能。你当然不希望在玩了很久游戏以后,忽然电脑死机,存档都丢失了吧。当然了,Wordle 比较简单,不需要恢复没有完成的游戏,只是需要记录历史成绩,所以实现起来更加简单。
如果没有指定 -S/--state 参数,则既不加载游戏状态,也不保存。
提示
在自动测试中,-S/--state 选项在 common.rs 中添加并传递给 Wordle 程序,因此在对应的 .args 文件中不出现 -S/--state 选项是正常的。如果要手动测试,请参考快速入门文档中相关内容。
- (0.25 分)增加命令行参数
-c/--config用于指定启动配置文件(如-c config.json),格式为如下的 JSON 文件:
{
"random": true,
"difficult": false,
"stats": true,
"day": 5,
"seed": 20220123,
"final_set": "fin.txt",
"acceptable_set": "acc.txt",
"state": "state.json",
"word": "cargo"
}
其中每个字段的含义和上面的命令行选项相同,并且所有字段都是可选的,但不能增加未定义的字段。如果同时在配置文件和命令行参数中指定了同一个参数(例如配置文件设置 "seed": 20220123 同时命令行参数设置了 --seed 20220234),则以后者为准,相当于配置文件设定了默认的命令行参数。
提示
为了让命令行参数优先级更高,即命令行参数可以覆盖配置文件的设置,建议把配置文件和命令行参数分别解析成自定义的 struct,然后合并两个 struct 得到最终的结果,这样可以简化实现。
也可以用现成的第三方库来做这件事情:config。