在大流行期间,Wordle 在 Twitter 上还算比较流行的一款基于网络的益智游戏,要求玩家每天在六次或更短时间内猜出一个新的五个字母的单词,每个人得到的单词都是一样的。
在本教程中,你将在终端上创建自己的 Wordle 克隆。自 2021 年 10 月 Josh Wardle 推出 Wordle 以来,已有数百万人玩过这款游戏。虽然您可以在网络上玩原版游戏,但您将以命令行应用程序的形式创建自己的版本,然后使用 Rich
库使其看起来更漂亮。
在这个循序渐进的项目中,您将学习如何建立一个简单的游戏原型,然后再将其迭代开发成一个可靠的应用程序。
在本教程中,您将学习如何:
- 从原型到完善的游戏,构建命令行应用程序
- 读取并验证用户输入
- 使用 Rich 的控制台在终端创建极具吸引力的用户界面
- 将代码组织成函数
- 为用户提供可操作的反馈
您将用 Python 创建自己的 Wordle 克隆 Wyrdl。本项目适用于任何想要从头开始创建终端应用程序的 Python 新手。在整个教程中,您将一步一步地创建代码,同时专注于创建一个从一开始就能玩的游戏。
演示: 您的 Python Wordle 克隆
在 Wordle 中,您有六次机会猜一个秘密的五字母单词。每次猜完后,你都会得到反馈,知道哪些字母放对了,哪些字母放错了,哪些字母放错了。
猜中后,每个字母都会被分类。正确的字母用绿色标出,错误的字母用黄色标出,错误的字母用灰色标出。
如果你犯了任何错误,比如猜出了一个有六个字母的单词,那么游戏会给你适当的反馈,让你再猜一次。
项目概述
本项目的一个重要部分是尽早引导应用程序。您希望代码能够运行,这样您就可以测试代码是否有效,还可以尝试以不同方式实现游戏中所需的功能。
您将通过以下步骤反复构建 Wordle 克隆:
-
创建一个简单的原型,让您猜出一个秘密单词,并对单个字母给出反馈。
-
加入游戏随机选择的单词列表,使游戏更加有趣。
-
重构代码,使用函数。
-
使用 Rich 库为游戏添加色彩和风格。
-
在用户玩游戏时为他们提供可操作的反馈。
-
通过添加字母表中所有字母的状态来改进用户界面。
在学习本教程的过程中,您将了解到如何从一个小创意开始,将其发展成为一个功能齐全的应用程序。毕竟,这就是 Wordle 的发展历程!
前提条件
在本教程中,您将使用 Python 和 Rich 创建一个 Wordle 克隆。在学习这些步骤时,如果您能熟练掌握以下概念,将对您的学习很有帮助:
- 在终端读取用户输入
- 使用 if 语句检查不同的条件
- 使用 for 和 while 循环重复操作
- 在列表和字典等结构中组织数据
- 用函数封装代码
是时候潜水了!
第 1 步:猜词
在这一步中,您将制作一个非常基本的猜词游戏。游戏看起来不会很好,游戏的反馈也很难解析。尽管如此,您的 Wordle 克隆的基石已经就位。这个动画展示了这一步结束时游戏的样子:
用户可以猜测单词,并了解哪些字母他们放对了,哪些字母他们放错了,哪些字母根本不在单词中。
在此步骤中,您将使用 input() 从播放器中读取单词,使用 for 循环让用户进行多次猜测,并使用集合找出用户猜对的字母。
使用 input() 获取用户信息
通过 input() 可以获取用户信息。这个内置函数是在命令行上提供简单交互性的好方法。
打开 REPL 试试看。编写以下内容:
>>> guess = input("Guess a word: ")
Guess a word: snake
>>> guess
'snake'
您可以为 input() 提供可选的提示。用户在输入任何信息前都会看到该提示。在上面的示例中,高亮显示的一行同时显示了提示和用户输入。提示符要求用户猜一个单词。用户输入 snake,然后点击 Enter。
调用 input() 会返回用户输入的文本。在上面的示例中可以看到,字符串 "snake "已被分配给 guess。
开始制作游戏永远不嫌早。打开编辑器,创建包含以下内容的 wyrdl.py 文件:
# wyrdl.py
guess = input("Guess a word: ")
if guess == "SNAKE":
print("Correct")
else:
print("Wrong")
读完用户的猜测后,检查他们的猜测是否等于密语 “SNAKE”。您将评估用户的猜测,并告知用户猜测的正确与否。
注意:通常情况下,创建的代码最好能尽早运行。即使代码只是做一些很小的事情,离你的最终目标还很远,让它可以运行也意味着你可以开始实验、测试和调试。
这可能并不像一场游戏。如果你把它当成游戏,它肯定是最无聊、最令人沮丧的游戏之一。游戏的可玩性很低,因为暗语总是一样的。对用户来说,反馈也不具可操作性,因为他们不会从被告知错误中学到任何东西。
你很快就会改进你的游戏,做出更有趣的游戏。在本小节的最后,您将解决一个小小的可用性问题。请看下面这个游戏:
$ python wyrdl.py
Guess a word: snake
Wrong
在这里,您猜对了暗语是 snake。但是,游戏却告诉你这是错误的,因为它将你的猜测与大写字符串 "SNAKE "进行了比较。在这个游戏中,我们的目标是猜出单词,而不是弄清楚字母是小写还是大写。如何比较两个单词的大小写呢?
最简单的办法可能是明确地将猜测转换为大写。这样,用户如何输入单词就无关紧要了:
# wyrdl.py
guess = input("Guess a word: ").upper()
if guess == "SNAKE":
print("Correct")
else:
print("Wrong")
您已经添加了 .upper() ,它可以强制用户的猜测为大写。这一操作立即让游戏变得更加友好:
$ python wyrdl.py
Guess a word: snake
Correct
现在,即使您用小写拼写,"snake "也会被报告为正确。不过,您只给了用户一次猜对的机会。在下一节中,您将通过更多的猜测来扩展您的游戏。
使用循环避免重复代码
在玩 Wordle 时,您最多有六次机会猜出正确的单词。要在游戏中实现同样的效果,一种方法是复制您已经写好的代码,然后重复六次。出于几个原因,这不是个好主意。最重要的是,它效率低下,维护复杂。
相反,您可以使用循环来实现重复行为。Python 支持两种主要的循环结构:for
和 while
。通常,在进行明确的迭代时,您会使用 for
,因为您事先知道要循环多少次。另一方面,while
非常适合不确定的迭代,当您不知道需要重复多少次某个操作时。
注意:还有其他创建循环的方法。有些编程语言依靠递归函数调用来创建循环。稍后,您还将看到 Python 中递归循环的示例。
不过一般来说,在 Python 中不应该使用递归来创建循环。函数调用相当慢,而且没有像其他语言那样对递归进行优化。通常,您最好坚持使用常规循环。
在本例中,我们要让用户猜六次单词,因此我们将使用 for
循环:
# wyrdl.py
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == "SNAKE":
print("Correct")
break
print("Wrong")
通过在一个范围内循环,您还可以计算猜测次数,并将该次数显示给用户:
$ python wyrdl.py
Guess 1: wyrdl
Wrong
Guess 2: snake
Correct
一旦用户找到了正确答案,就没有必要让他们继续猜了。如果用户猜对了单词,就可以使用 break
语句提前跳出循环。引入 break
语句的另一个好处是,你不再需要明确的 else
语句。只有当用户猜错时,代码才会继续。
现在是时候为用户添加一些适当的反馈,使游戏具有可玩性了。在下一小节中,你将看到如何枚举用户猜对的字母。
使用集合检查字母
到目前为止,你只告诉了用户他们是否猜对了单词。为了给用户提供一些提示,让他们可以利用这些提示推断出暗语,我们将添加对用户猜中的单个字母的反馈。您将把每个字母分为三类:
- 正确(Correct)的字母在密语中出现的位置与猜测的位置相同。
- 错误(Misplaced)的字母出现在密语中,但位置不同。
- 错误(Wrong)的字母不出现在密语中。
例如,如果密语是 SNAKE,那么您可以将一些猜测中的字母分类如下:
Guess | Correct letters | Misplaced letters | Wrong letters |
---|---|---|---|
BLACK | A | K | B, C, L |
ADDER | A, E | D, R | |
LEARN | A | E, N | L, R |
QUAKE | A, E, K | Q, U | |
CRANE | A, E | N | C, R |
SNAKE | A, E, K, N, S | ||
WYRDL | D, L, R, W, Y |
如何找出哪些字母属于哪个类别?首先要找出哪些字母的位置是正确的。Python 的 zip()
函数非常适合对两个序列进行逐元素比较。在本例中,您要比较两个字符串的字母:
>>> for snake_letter, crane_letter in zip("SNAKE", "CRANE"):
... if snake_letter == crane_letter:
... print(snake_letter)
...
A
E
在此代码片段中,您通过逐个比较 SNAKE 和 CRANE,找出 CRANE 中位置正确的两个字母。尽管两个单词中都有一个 N,但由于位置不同,所以没有报告。
现在,您需要收集字母,而不仅仅是打印出来。综合是 Python 中一种强大的结构,用于将一个或多个序列转换成另一个序列。在这里,您将使用集合理解来收集正确的字母:
>>> word = "SNAKE"
>>> guess = "CRANE"
>>> {letter for letter, correct in zip(guess, word) if letter == correct}
{'A', 'E'}
集合理解类似于列表理解,但输出的是集合而不是列表。它在这种情况下非常有效,因为正确字母的顺序并不重要。
集合的一个优点是 Python 提供了强大的操作。您可以快速使用两个集合之间的联合(unions)、交集(intersections)和差集(differences)来查找至少出现在一个集合中、两个集合中或只出现在其中一个集合中的元素。
例如,如果您有两个字符串,那么您可以使用集合相交 (&) 来查找出现在两个字符串中的所有字母:
>>> set("SNAKE") & set("CRANE")
{'A', 'E', 'N'}
交集告诉我们 A、E 和 N 同时出现在 SNAKE 和 CRANE 中。同样,您也可以使用集合差来查找出现在一个集合中而不出现在另一个集合中的字母:
>>> set("CRANE") - set("SNAKE")
{'C', 'R'}
事实上,CRANE 中的字母 C 和 R 在 SNAKE 中没有出现。
注:集合交集 (&)、集合联合 (|) 和集合差集 (-) 操作符用于在 Python 中实现集合论操作。它们不同于布尔逻辑操作符,如 and
、or
和 not
。
因为操作数是集合,所以 Python 可以理解您不是在使用 - 作为算术运算符。
是时候使用集合来改进您的游戏了。不过,在开始实现之前,您还需要做一个改变。目前,您将密语硬编码到 if 测试中。在对字母进行分类时,您会用到这个单词,因此您将使用常量来引用它:
# wyrdl.py
WORD = "SNAKE"
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == WORD:
print("Correct")
break
print("Wrong")
引入 WORD 后,更改暗语变得更加容易。在下一节中,你将添加一个单词表,从中选择单词,使游戏更加有趣。
利用上文关于集合的内容,您现在可以计算并显示正确、错位和错误的字母。更新你的代码,使它看起来像下面这样:
# wyrdl.py
WORD = "SNAKE"
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == WORD:
print("Correct")
break
correct_letters = {
letter for letter, correct in zip(guess, WORD) if letter == correct
}
misplaced_letters = set(guess) & set(WORD) - correct_letters
wrong_letters = set(guess) - set(WORD)
print("Correct letters:", ", ".join(sorted(correct_letters)))
print("Misplaced letters:", ", ".join(sorted(misplaced_letters)))
print("Wrong letters:", ", ".join(sorted(wrong_letters)))
您要利用集合理解来找到所有正确摆放的字母。放错位置的字母是指同时出现在猜测词和密语中但没有正确摆放的字母。最后,猜测中没有出现在暗语中的字母被归类为错误。
目前,您只需列出类别和字母。例如:
$ python wyrdl.py
Guess 1: crane
Correct letters: A, E
Misplaced letters: N
Wrong letters: C, R
Guess 2: snake
Correct
虽然信息是存在的,但却不那么容易理解。稍后,您将改进用户界面,使游戏既好看又好玩儿。不过,下一步工作是使用单词表来增加游戏的多样性。
步骤 2:使用单词表
在这一步中,您不会改变游戏的功能。不过,通过添加单词表,你可以让游戏更有趣、更耐玩。到目前为止,密语一直都是一样的。这一点即将改变:
游戏看起来还是一样,但你每次玩都要猜一个不同的单词。
在本步骤中,您将首先手动创建一个小型单词表,并将其整合到您的游戏中。然后,您将了解如何将任何文本转化为单词表。
手动创建单词表
单词表将是一个纯文本文件,每行包含一个单词。这沿袭了 Unix 系统的悠久传统,拼写检查程序和类似应用程序都会使用名为单词的文件。
开始时,创建一个新文件,命名为 wordlist.txt,内容如下:
adder
black
crane
learn
quake
snake
wyrdl
您已经添加了上一步中作为可能的猜测而调查的单词。您可以自行扩展单词表。不过,不要花太多精力,因为你很快就会自动创建单词表。
在创建更好的单词表之前,我们先来看看如何将单词表读入程序。Python 的 pathlib
模块非常适合处理不同的文件并将它们读入内存。试试吧:
>>> import pathlib
>>> pathlib.Path("wordlist.txt").read_text(encoding="utf-8")
'adder\nblack\ncrane\nlearn\nquake\nsnake\nwyrdl\n'
.read_text()
方法将整个文件作为一个文本字符串读取。请注意,单词之间用 \n
符号分隔。这些符号代表文件中的换行符。你可以去掉最后一个换行符,然后分隔其余的换行符,将文件转换成一个单词列表:
>>> WORDLIST = pathlib.Path("wordlist.txt")
>>> [
... word.upper()
... for word in WORDLIST.read_text(encoding="utf-8").strip().split("\n")
... ]
['ADDER', 'BLACK', 'CRANE', 'LEARN', 'QUAKE', 'SNAKE', 'WYRDL']
您可以使用 .strip()
删除文件末尾多余的行,使用 .split() 将文件转换为单词列表。为了避免小写和大写的麻烦,您还会将所有单词转换为大写。
注意:在玩原始 Wordle 游戏时,您只能猜测实际单词。在您的 Wordle 克隆版中不会有同样的限制,因为它需要一个详尽的单词列表。有限的单词列表会让用户感到沮丧,因为他们会试图找出您在允许的列表中包含了哪些单词。
您现在可以在程序中包含一个单词列表。如何从单词列表中随机选择一个单词呢?
从单词列表中选择随机单词
Python 标准库中有一个功能强大的随机模块。您可以用它在项目中生成各种随机性。在这里,您将使用 random.choice()
,它可以从一个序列中随机选择一个项目:
>>> import random
>>> random.choice(["SNAKE", "ADDER", "CRANE"])
'CRANE'
>>> random.choice(["SNAKE", "ADDER", "CRANE"])
'ADDER'
>>> random.choice(["SNAKE", "ADDER", "CRANE"])
'ADDER'
如果运行相同的代码,结果可能会有所不同。
是时候为您的 Wordle 克隆添加单词列表功能了。按以下步骤编辑您的游戏:
# wyrdl.py
import pathlib
import random
WORDLIST = pathlib.Path("wordlist.txt")
words = [
word.upper()
for word in WORDLIST.read_text(encoding="utf-8").strip().split("\n")
]
word = random.choice(words)
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == word:
print("Correct")
break
correct_letters = {
letter for letter, correct in zip(guess, word) if letter == correct
}
misplaced_letters = set(guess) & set(word) - correct_letters
wrong_letters = set(guess) - set(word)
print("Correct letters:", ", ".join(sorted(correct_letters)))
print("Misplaced letters:", ", ".join(sorted(misplaced_letters)))
print("Wrong letters:", ", ".join(sorted(wrong_letters)))
您已经在脚本顶部添加了代码,可以读取单词列表并从中随机选择一个单词。由于密语不再是固定不变的,因此您还将 WORD 更名为 word。
一个小问题是,如果你没有猜对单词,就永远无法知道 Wordle 克隆随机选择的是哪个密词。要解决这个问题,可以在代码末尾添加以下内容:
# wyrdl.py
# ...
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == word:
print("Correct")
break
# ...
else:
print(f"The word was {word}")
在 for
中使用 else
子句并不常见,但在正确的使用情况下,它的功能却相当强大。如果 for
循环没有自然终止,即如果 break
停止了循环,else
内的代码就会运行。实际上,这意味着如果所有猜测都与 word 不同,就会打印出密文。
注意:在开发游戏的过程中,您会多次运行游戏。为了有效测试代码,您可能需要作弊,事先知道暗语。在测试代码时,您可以在调用 input() 之前添加以下一行:
print(word)
这将把暗语打印到控制台。
多试几次你的游戏。由于事先不知道确切的单词,因此游戏已经更具挑战性和趣味性。尽管如此,由于单词列表有限,游戏还是会变得重复。接下来,您将看到如何创建更大的单词表。
将文本转换为单词表
你的系统中可能已经有一个单词表,你也可以从网上下载单词表。不过,为了提高灵活性和控制性,你可能还是想创建自己的列表。例如,你可以创建包含编程相关术语、城市名称或非英语单词的特殊主题单词表。
您将创建一个脚本,将任何文本文件转换为格式精美的单词列表,供您在 Wordle 克隆中使用。在项目中添加一个名为 create_wordlist.py 的新文件,内容如下:
# create_wordlist.py
import pathlib
import sys
from string import ascii_letters
in_path = pathlib.Path(sys.argv[1])
out_path = pathlib.Path(sys.argv[2])
words = sorted(
{
word.lower()
for word in in_path.read_text(encoding="utf-8").split()
if all(letter in ascii_letters for letter in word)
},
key=lambda word: (len(word), word),
)
out_path.write_text("\n".join(words))
脚本使用 sys.argv
从命令行读取信息。特别是,你需要提供现有文本文件的路径和新单词表文件的位置。前两个命令行参数被转换为路径,并在第 7 行和第 8 行分别命名为 in_path
和 out_path
。
例如,你可以使用脚本将当前版本的 wyrdl.py 转换为单词表,如下所示:
$ python create_wordlist.py wyrdl.py wordlist.txt
它会读取 wyrdl.py,查找单词并将其存储到当前目录下的 wordlist.txt 中。请注意,这将覆盖您手动创建的 wordlist.txt。看看你的新单词表吧:
if
in
for
word
break
guess
words
import
letter
random
correct
pathlib
wordlist
您会从代码中认出一些单词。不过,请注意只有部分单词进入了单词表。回看 create_wordlist.py,特别注意第 14 行。这一行在你的集合理解中起着过滤器的作用,它不会通过包含任何非 ASCII 字符的单词。实际上,它只允许字母 A 到 Z。
注意:只允许使用字母 A 到 Z 可能限制太多,尤其是如果您想创建英语以外语言的单词表。在这种情况下,可以使用正则表达式。\w 特殊序列可以匹配任何语言中单词的大部分字符。
将第 14 行替换为下面的内容,可以得到一个更宽松的过滤器:
if re.fullmatch(r"\w+", word):
如果采用这种方法,记得在代码顶端导入 re。该过滤器还允许使用下划线,因此像 guess_num 这样的词将被包含在单词列表中。使用 r"[^\W0-9_]+" 可以避免下划线和数字。这将使用负字符组,列出单词中不允许出现的字符。
请注意,您不会过滤掉长度大于或小于五个字母的单词。您可以在构建单词表时这样做。不过,如果把这项工作留给 wyrdl.py 代码来做,就能获得一定的灵活性。这样就可以使用通用单词表,并在游戏中改变单词长度。也许您想创建一个 Wordle 变体,让用户测试七个字母的单词。
您还可以对单词列表进行排序。严格来说,这并不是必须的,但可以方便手动浏览列表。在第 16 行,你指定了 key 来定制排序顺序。
通过 key=lambda word: (len(word), word),你最终会首先根据每个单词的长度排序,然后再根据单词本身排序。其结果是,单词列表从所有单字母单词开始,然后是双字母单词,以此类推。每批长度相同的单词按字母顺序排序。
现在,您可以生成个人单词表了。找到任何纯文本文件,运行 create_wordlist.py 脚本。例如,你可以下载莎士比亚全集来创建一个老式的单词表,或者下载一个用单音节单词重述的《爱丽丝梦游仙境》版本来创建一个更简单的单词表。
由于单词表现在包含的单词长度不是五个字母,因此您应该在列表理解中添加一个过滤器来解析单词表。添加以下过滤器:
# wyrdl.py
import pathlib
import random
from string import ascii_letters
# ...
words = [
word.upper()
for word in WORDLIST.read_text(encoding="utf-8").split("\n")
if len(word) == 5 and all(letter in ascii_letters for letter in word)
]
# ...
您还可以删除 .strip() ,因为在 if 测试中,空字已经被过滤掉了。展开以下方框,查看此时的完整源代码:
# wyrdl.py
import pathlib
import random
from string import ascii_letters
WORDLIST = pathlib.Path("wordlist.txt")
words = [
word.upper()
for word in WORDLIST.read_text(encoding="utf-8").split("\n")
if len(word) == 5 and all(letter in ascii_letters for letter in word)
]
word = random.choice(words)
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
if guess == word:
print("Correct")
break
correct_letters = {
letter for letter, correct in zip(guess, word) if letter == correct
}
misplaced_letters = set(guess) & set(word) - correct_letters
wrong_letters = set(guess) - set(word)
print("Correct letters:", ", ".join(sorted(correct_letters)))
print("Misplaced letters:", ", ".join(sorted(misplaced_letters)))
print("Wrong letters:", ", ".join(sorted(wrong_letters)))
else:
print(f"The word was {word}")
由于篇幅较长,我就拆成几篇方便阅读。如有兴趣,可以持续关注我的动态哦!