跳转至

Wordle 详细功能要求

本页从往届 Wordle 大作业文档整理而来,只保留本届 Lab 4 要实现的八组基础功能。

本页可以在简洁版和详细版之间切换:

切换版本

  • 简洁版本去掉了提示,只保留作业要求
  • 详细版本包括所有内容

刚开始时建议选择详细版;熟悉内容后,可以切换到简洁版检查自己的实现。

当前版本:简洁版本
当前版本:详细版本

基础功能要求

后面的功能会用到前面已经实现的内容,因此建议按顺序完成。正式评分时,下面八组功能测试分别计分。

八组基础功能使用 Rust 的测试框架进行自动测试。为了便于测试,根据标准输出是否为交互式终端(项目模板中提供了 is_tty 用于判断),你的程序必须实现两种输出模式:

  • 如果是,则使用较为友好和直观的输出(称为交互模式)。
  • 如果不是,则严格遵守下面定义的输出格式(称为测试模式)。如果有任何不可恢复的错误(如参数格式错误、文件不存在等),程序必须以非正常返回值退出。

在项目根目录运行 cargo test -- --test-threads=1 可以检查基础功能。

关于测试的注意事项

  • 公开测试只覆盖部分输入;正式评分还会使用助教保留的测试,因此仍需完整实现下面的要求。
  • 每个测试点均有运行时间限制,超时将会直接认为失败。

如无特别说明,下面所有涉及到 Wordle 游戏中字母的打印都必须全部大写

  • (0.25 分)程序启动时,从标准输入读取一个词作为答案,开始一局游戏(一局游戏最多 6 次猜测)。候选词与可用词内嵌在程序中(分别对应代码 src/builtin_words.rs 中的 FINALACCEPTABLE)。从标准输入读取一行用户的猜测词:
    • 在每次猜测(获得可用词列表中的输入)后打印猜测结果以及所有字母的状态:
      • 测试模式:程序启动后,不进行任何输出,等待用户的输入。在测试模式下可以认为输入的答案词符合要求。每次猜测后,如果输入符合要求,则打印一行,形如 SSSSS AAAAAAAAAAAAAAAAAAAAAAAAAA,前面五个字母是用户的猜测结果,后面 26 个字母是所有字母的状态(类似 Wordle 游戏中的输入键盘)。SA 允许的取值包括 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 用于启动随机模式,在随机模式下,不再采用指定答案模式,而是从候选词库中(项目模版中给定)随机抽取问题。随机模式和指定答案模式只能二选一,默认情况下是指定答案模式。程序在测试模式下的输出格式不变。

    解析命令行参数的方法

    解析命令行参数有两种方法:

    1. 遍历 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
      
    2. 自学第三方库 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 指定了答案,则退出
    • 交互模式:以自定义方式显示信息

    多关键字排序

    多关键字排序(这里涉及到两个关键字,分别是猜测次数和猜测词)有两种实现方法:

    1. 使用稳定排序算法,先排序一个关键字,再排序另一个关键字。想想应该先排哪一个,后排哪一个?为什么需要稳定排序算法?
    2. 自定义一个比较函数,比较的时候,把两个关键字都考虑进来,然后一次性排好序
  • (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\) 不允许是恒等变换(即不同的种子必须对应不同的排列)
    • 为了方便检查,规定在实现中,必须使用 rand crate(版本为 0.8.5)提供的 shuffle 方法,随机数引擎必须使用 rand::rngs::StdRng,并把 s 作为种子传入。具体来说,你需要先用 s 作为种子初始化一个 rand::rngs::StdRng(使用 seed_from_u64(u64)),再用这个 StdRngshuffle 候选词库,最后以 day - 1 作为下标访问打乱后的数组。

    数组打乱的方法

    为了保证多次随机出来的数不重复,把所有可能的值放在一个数组中,随机打乱以后,按顺序取出来,这样做既保证了随机性,又保证了不重复性。

    shuffle 函数实现了 Fisher-Yates 算法,这是一个很经典的数组打乱算法,值得学习。

  • (0.25 分)增加命令行参数 -f/--final-set 以及 -a/--acceptable-set 用于指定候选词库和可用词库文件(如 -f final.txt -a acceptable.txt)。如果不指定,则使用内置的词库。文件的格式均为按行分割的单词列表(不区分大小写)。加载时需要检查是否符合格式要求、是否存在重复,并且候选词库必须严格是可用词库的子集。如果在指定答案模式中,则也要检查用户给定的答案是否在候选词库中。在读入词库后,需要将其全部转为大写,并按字典序排序

    提示

    读取文件内容,可以用 Rust 标准库中的函数,如:

    检查是否存在重复的元素,以及判断两个集合之间是否为子集的关系,可以用 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 标准库中的函数,如:

JSON 是一种数据交换格式,支持数组、字典(Map)、字符串、数字、bool 和 null。具体的表达方式如下:

  1. 数组:以 [ 开头,接着是以 , 分隔的数组元素,最后以 ] 结尾。元素可以是任意类型。
  2. 字典(Map):以 { 开头,接着是若干个键(Key)值(Value)对最后以 } 结尾。键值对之间用 , 分隔,键和值之间用 : 分隔。键必须是字符串,值可以是任意类型。
  3. 字符串:用两个 " 括起来的字符串,支持转义字符。
  4. 数字:十进制数,支持小数。
  5. bool:true 或者 false。
  6. null

读写 JSON 有两种方法:

  1. 自行解析 JSON,例如:找到第一个 { 字符,然后找到前两次出现的 " 字符,提取出双引号之间的字符串,判断它是 total_rounds 还是 games;如果是 total_rounds,找到 :, 的位置,提取出中间的数字;如果是 games,那么找到 [,再找到 {,重复类似前面的过程,分别解析出 answerguesses 的内容。最后把数据放在一个自定义的 struct 中。注意字典里面键(Key)的顺序是不固定的,不保证 total_round 一定出现在 games 之前。
  2. 使用 serde_jsonjson 库来操作 JSON。推荐阅读 How to Work With JSON in RustRust - 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