在大流行期间,Wordle 在 Twitter 上还算比较流行的一款基于网络的益智游戏,要求玩家每天在六次或更短时间内猜出一个新的五个字母的单词,每个人得到的单词都是一样的。
在本教程中,你将在终端上创建自己的 Wordle 克隆。自 2021 年 10 月 Josh Wardle 推出 Wordle 以来,已有数百万人玩过这款游戏。虽然您可以在网络上玩原版游戏,但您将以命令行应用程序的形式创建自己的版本,然后使用 Rich
库使其看起来更漂亮。
书接上回
第 3 步:用函数组织代码
到目前为止,您已经将游戏写成了脚本。它本质上是一个接一个运行的命令列表。虽然这对于快速上手和测试游戏的简单原型来说很不错,但这类程序并不能很好地扩展。随着程序复杂度的增加,您需要将代码归类为可以重复使用的函数。
在这一步结束时,对于用户来说,游戏看起来还是一样的。但底层代码将更容易扩展和构建。
首先,您要明确设置游戏的主循环。然后,将辅助代码移到函数中。最后,您将考虑如何测试您的游戏,以确保它按照您的预期运行。
设置主循环
到目前为止,您已经建立了 Wyrdl 的基本版本。将其视为一个原型,您已经测试了游戏中的一些功能,并对游戏中的重要功能有了一定的了解。
现在,您将对代码进行重构,为下一步的扩展和改进做好准备。您将创建一些函数,作为程序的构件。
要想知道哪些函数在你的程序中有用,你可以做一个小练习,自上而下地思考程序中的功能。在高层次上,你的程序流程是怎样的?在继续之前,请自行尝试。展开下面的方框,查看一种可能的解决方案:
下图举例说明了如何描述程序的主要流程。点击该图放大查看细节:
从图中可以看出,您的游戏首先会得到一个随机单词,然后进入一个用户猜词的循环,直到用户猜对或猜完为止。
请注意,您不需要在本图中说明太多细节。例如,您不必担心如何获得随机单词或如何检查用户的猜测。您只需注意应该这样做。
下一步是将图表转化为代码。在 wyrdl.py 文件底部添加以下内容。先不要删除任何现有代码,因为你很快就会用到:
# wyrdl.py
# ...
def main():
# Pre-process
word = get_random_word(...)
# Process (main loop)
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
show_guess(...)
if guess == word:
break
# Post-process
else:
game_over(...)
高亮显示的几行表明,main() 调用了三个还不存在的函数:get_random_word()
、show_guess()
和 game_over()
。您很快就会创建这些函数,但现在,您可以尽情享受想象这些构件可用的自由。
main() 内的代码也分为三个部分:前处理、处理和后处理。一旦你习惯了识别程序的主要流程,你就会发现通常可以这样划分:
- 前处理(Pre-process)包括主循环运行前需要发生的所有事情。
- 过程(Process )是程序在主循环期间所做的工作。
- 后处理(Post-process)是主循环结束后的清理工作。
在您的 Wordle 克隆中,您会在主循环之前随机选择一个单词,并在主循环之后让用户知道游戏已经结束。在主循环期间,您要处理用户的猜测。主循环可能以两种方式之一结束:用户猜对了或者猜错了太多。
不幸的是,一厢情愿并不足以让 main() 正常工作。在下一节中,您将实现缺失的函数。
创建辅助函数
目前,你的 main() 函数无法运行。你还没有实现 get_random_word()、show_guess() 和 game_over()。这种情况很糟糕,因为如果无法运行函数,就无法对其进行测试,以确保它能完成预期的功能。现在你要实现这三个函数,主要是移动你之前写的代码。
首先考虑 get_random_word()。这个函数应该做什么?在实现时,您可以使用以下要求作为指导:
- 从现有单词列表中随机选择一个单词。
- 确保单词长度为五个字母。
在实现一个新函数时,一个重要的决定因素是函数应接受哪些参数。在本例中,您可以输入单词表或单词表路径。不过,为了简单起见,您将在函数中硬编码单词表的路径。这意味着您不需要任何参数。
在源代码中添加以下函数。请注意,您已经编写了 get_random_word() 中的大部分代码。您可以将之前实现中的代码移到该函数中:
# wyrdl.py
# ...
def get_random_word():
wordlist = pathlib.Path(__file__).parent / "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)
]
return random.choice(words)
如前所述,您需要从文件中读取单词表,然后过滤单词表,以便得到长度正确的单词。每次获得新单词时读取单词表可能会很慢。不过,在本游戏中,您只需调用一次 get_random_word(),所以这不是问题。
下一个需要实现的函数是 show_guess()。这段代码将与您当前代码的以下部分相对应:
# ...
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)))
# ...
您需要比较用户的猜测和密语。当把这个过程转移到函数中时,您需要确定函数将接受哪些参数,其返回值应该是什么。
在本例中,您需要输入用户的猜测和正确的单词。函数将在控制台中显示结果,因此不需要返回任何内容。将代码移到下面的函数中:
# wyrdl.py
# ...
def show_guess(guess, word):
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)))
新函数首先会将用户猜测的字母分为正确字母、错位字母和错误字母。然后将这些字母打印到控制台。
现在要实现的最后一个函数是 game_over()。目前,将它重构为一个单独的函数可能有些矫枉过正,因为它只会向屏幕上打印一条信息。不过,通过这样划分代码,您就可以命名代码的特定部分,清楚地说明代码在做什么。如果需要,还可以在以后扩展代码。
如前所述,如果用户无法猜出单词,您需要告诉他们单词是什么。为此,您可以添加以下函数:
# wyrdl.py
# ...
def game_over(word):
print(f"The word was {word}")
您的函数接受 word 作为参数,并用 f-string 将其打印到终端以通知用户。
现在,您可以对之前设置的 main() 进行最后的调整。尤其是需要填入作为占位符的省略号,并调用 main() 来启动游戏。
更新 main() 如下:
# wyrdl.py
# ...
def main():
# Pre-process
word = get_random_word()
# Process (main loop)
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
show_guess(guess, word)
if guess == word:
break
# Post-process
else:
game_over(word)
你已经为每个函数调用添加了必要的参数。要完成重构,可以删除函数定义之外的代码(导入除外)。然后在源文件末尾添加以下内容,使用 name-main 习语调用 main():
# wyrdl.py
# ...
if __name__ == "__main__":
main()
这些行将确保在执行文件时调用你的代码。
在这一步中,你已经修改了整个文件。要检查代码的当前状态,可以展开下面的部分并进行比较。
# wyrdl.py
import pathlib
import random
from string import ascii_letters
def main():
# Pre-process
word = get_random_word()
# Process (main loop)
for guess_num in range(1, 7):
guess = input(f"\nGuess {guess_num}: ").upper()
show_guess(guess, word)
if guess == word:
break
# Post-process
else:
game_over(word)
def get_random_word():
wordlist = pathlib.Path(__file__).parent / "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)
]
return random.choice(words)
def show_guess(guess, word):
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)))
def game_over(word):
print(f"The word was {word}")
if __name__ == "__main__":
main()
完成所有这些更改后,您的游戏应该可以正常运行了。运行代码,确保游戏能正常运行。
第 4 步:用 "丰富 "打造游戏风格
在上一步中,您为更大的改变奠定了基础。现在是大幅改善游戏用户体验的时候了。您将使用 Rich 库为终端中的文本添加颜色和样式:
如果您玩过 Wordle 在线游戏,那么您一定会认出猜测表和表示字母正确、错位或错误的彩色字母。
了解 Rich 控制台打印机
Rich 最初由 Will McGugan 开发,目前由 Will 的公司 Textualize.io 维护。Rich 可以帮助你在终端中对文本进行着色、样式和格式化。
注:Rich 是 Textual 的主要构建模块。Textual 是构建文本用户界面(TUI)的框架。本教程中不会使用 Textual。不过,如果你想在终端内创建成熟的应用程序,请查看本教程。
Rich 是一个第三方库,使用前需要安装。在安装 Rich 之前,应创建一个虚拟环境,以便安装项目依赖项。在下面选择您的平台,然后键入以下命令:
PS> python -m venv venv
PS> venv\Scripts\Activate
(venv) PS>
$ python -m venv venv
$ source venv/bin/activate
(venv) $
创建并激活虚拟环境后,就可以使用 pip 安装 Rich:
(venv) $ python -m pip install rich
安装 Rich 后,您就可以试用了。使用 Rich 的快速入门方法是覆盖 print() 函数:
>>> from rich import print
>>> print("Hello, [bold red]Rich[/] :snake:")
Hello, Rich 🐍
虽然在此代码块中没有显示,但 Rich 将以红色粗体显示 Rich 一词。Rich 使用自己的标记语法,其灵感来自 Bulletin Board Code。你可以在方括号中添加样式指令,如上面的 [bold red]。在用 [/] 关闭之前,该样式一直有效。
你还可以使用冒号括起来的表情符号名称来打印表情符号。在上面的例子中,你使用 🐍 来打印蛇表情符号 (🐍)。运行 python -m rich.emoji
查看所有可用表情符号的列表。
注意:Windows 10 及更早版本对表情符号的支持有限。不过,你可以安装 Windows 终端来获得完整的 Rich 体验。大多数 Linux 和 macOS 终端都能很好地支持表情符号。
像这样重写 print() 可能会很方便,但从长远来看并不灵活。使用 Rich 的首选方法是初始化一个 Console 对象并将其用于打印:
>>> from rich.console import Console
>>> console = Console()
>>> console.print("Hello, [bold red]Rich[/] :snake:")
Hello, Rich 🐍
和上面一样,这将以粗体红色输出 Rich。
使用 Rich 使游戏更美观的一种方法是在两次猜测之间清除屏幕。可以通过 console.clear() 来实现。在代码中添加以下函数:
# wyrdl.py
# ...
def refresh_page(headline):
console.clear()
console.rule(f"[bold blue]:leafy_green: {headline} :leafy_green:[/]\n")
# ...
在这里,console.clear()
将清空屏幕。然后,console.rule()
将在屏幕上方打印一个标题。使用 rule()
,您将添加一条水平规则作为装饰,为打印文本增加一些分量:
>>> from rich.console import Console
>>> console = Console(width=40)
>>> console.rule(":leafy_green: Wyrdl :leafy_green:")
───────────── 🥬 Wyrdl 🥬 ──────────────
由于 refresh_page() 指向控制台,因此需要导入 Rich 并在代码顶部初始化一个控制台对象:
# wyrdl.py
import pathlib
import random
from string import ascii_letters
from rich.console import Console
console = Console(width=40)
# ...
您可以指定控制台的宽度。这在使用 rule() 等元素时非常有用,因为这些元素会展开以填充整个宽度。如果不指定宽度,Rich 将使用终端的实际宽度。
Rich 的一个显著特点是可以添加自定义样式。举例来说,你可以添加一种样式,在用户做错事时发出警告。为此,您可以实例化主题,并将其传递给 Console:
# wyrdl.py
import pathlib
import random
from string import ascii_letters
from rich.console import Console
from rich.theme import Theme
console = Console(width=40, theme=Theme({"warning": "red on yellow"}))
# ...
这会将警告添加为一种新样式,显示为黄底红字:
稍后,当您在游戏中添加用户验证时,就会用到这种样式。您还可以在 REPL 中快速测试 refresh_page():
>>> import wyrdl
>>> wyrdl.refresh_page("Wyrdl")
───────────── 🥬 Wyrdl 🥬 ─────────────
>>> wyrdl.console.print("Look at me!", style="warning")
Look at me!
输入代码后,您会看到屏幕在打印 Wyrdl 标题之前被清空。接下来,"看着我!"将以黄底红字的警告样式打印出来。
跟踪之前的猜测并为其着色
如果在两次竞猜之间清空屏幕,游戏看起来会更整洁,但用户也会错过一些有关之前竞猜的关键信息。因此,您需要跟踪之前的猜测,并向用户显示相关信息。
为了记录所有的猜测,您将使用一个列表。您可以用"_____"(五个下划线)来初始化列表,作为未来猜测的占位符。然后,当用户进行猜测时,你将覆盖占位符。
首先更新 main() 如下:
# wyrdl.py
# ...
def main():
# Pre-process
words_path = pathlib.Path(__file__).parent / "wordlist.txt"
word = get_random_word(words_path.read_text(encoding="utf-8").split("\n"))
guesses = ["_" * 5] * 6
# Process (main loop)
for idx in range(6):
guesses[idx] = input(f"\nGuess {idx + 1}: ").upper()
show_guess(guesses[idx], word)
if guesses[idx] == word:
break
# Post-process
else:
game_over(word)
# ...
您将 guesses 添加为包含所有猜测的列表。由于该列表的索引为零,因此要将 range 改为 range(6),使其从 0 到 5,而不是从 1 到 6。这样,你就可以用 guesses[idx] 代替 guess 来引用当前的猜测值了。
接下来,您将更新显示用户猜测的方式。新函数将把所有猜测打印到屏幕上,并使用 Rich 制作漂亮的颜色和格式。由于要为每个字母选择合适的颜色,因此要循环显示每个猜测中的字母。
为方便起见,您需要改变对每个字母的分类方式,摆脱之前使用的基于集合的逻辑。用以下代码将 show_guess() 替换为 show_guesses():
# wyrdl.py
# ...
def show_guesses(guesses, word):
for guess in guesses:
styled_guess = []
for letter, correct in zip(guess, word):
if letter == correct:
style = "bold white on green"
elif letter in word:
style = "bold white on yellow"
elif letter in ascii_letters:
style = "white on #666666"
else:
style = "dim"
styled_guess.append(f"[{style}]{letter}[/]")
console.print("".join(styled_guess), justify="center")
# ...
对于每个猜测,你都要创建一个样式字符串,将每个字母包裹在一个标记块中,并添加相应的颜色。要对每个字母进行分类,可以使用 zip() 并行循环查看猜测和密语中的字母。
如果字母是正确的,那么就用绿色背景对其进行样式处理。如果字母放错了位置,即字母不正确,但在密语中,则添加黄色背景。如果字母是错的,那么就用灰色背景来表示,这里用十六进制代码 #666666 表示。最后,以暗淡的样式显示占位符。
通过使用 console.print(),Rich 可以正确显示颜色。为了使猜测表排列整齐,你使用了 justify 来使每个猜测居中。
确保删除旧的 show_guess() 函数。在使用新函数显示用户猜测之前,需要更新 main() 以调用该函数:
# wyrdl.py
# ...
def main():
# Pre-process
words_path = pathlib.Path(__file__).parent / "wordlist.txt"
word = get_random_word(words_path.read_text(encoding="utf-8").split("\n"))
guesses = ["_" * 5] * 6
# Process (main loop)
for idx in range(6):
refresh_page(headline=f"Guess {idx + 1}")
show_guesses(guesses, word)
guesses[idx] = input("\nGuess word: ").upper()
if guesses[idx] == word:
break
# Post-process
else:
game_over(word)
# ...
请注意,现在是在获取用户新的猜测之前显示猜测结果。这是必要的,因为 refresh_page() 会清除屏幕上所有之前的猜测。
运行代码。如果一切按预期运行,那么您应该会看到您的猜测以漂亮的颜色排成一行:
在游戏过程中,您会发现基本的 game_over()现在感觉有点格格不入。在下一节中,您还将对游戏的结尾进行 Rich 处理。
有条不紊地结束游戏
当前的 game_over() 实现存在一个问题,那就是它不会根据最终猜测更新猜测表。出现这种情况的原因是您将 show_guesses() 放在了 input() 之前。
您可以在 game_over() 中调用 show_guesses()来解决这个问题:
# wyrdl.py
# ...
def game_over(guesses, word):
refresh_page(headline="Game Over")
show_guesses(guesses, word)
# ...
# wyrdl.py
# ...
def main():
# Pre-process
words_path = pathlib.Path(__file__).parent / "wordlist.txt"
word = get_random_word(words_path.read_text(encoding="utf-8").split("\n"))
guesses = ["_" * 5] * 6
# Process (main loop)
for idx in range(6):
refresh_page(headline=f"Guess {idx + 1}")
show_guesses(guesses, word)
guesses[idx] = input("\nGuess word: ").upper()
if guesses[idx] == word:
break
# Post-process
# Remove else:
game_over(guesses, word)
# ...
无论用户是否猜对了单词,您都希望调用 game_over()。这意味着您不再需要 else 子句,因此您可以删除它。
现在,您的游戏可以正确显示最终猜测结果。但是,用户并没有得到关于他们是否能正确猜出密语的反馈。
在 game_over() 的末尾添加以下几行:
# wyrdl.py
# ...
def game_over(guesses, word, guessed_correctly):
refresh_page(headline="Game Over")
show_guesses(guesses, word)
if guessed_correctly:
console.print(f"\n[bold white on green]Correct, the word is {word}[/]")
else:
console.print(f"\n[bold white on red]Sorry, the word was {word}[/]")
# ...
您添加了一个新参数 guessed_correctly,用来向用户提供正确的反馈。要完成这次重构,您需要在调用 game_over() 时传入正确的值:
# wyrdl.py
# ...
def main():
# ...
# Post-process
game_over(guesses, word, guessed_correctly=guesses[idx] == word)
# ...
您可以将最后一次猜测与密语进行比较,以确定用户是否猜对了单词。
测试您的游戏。它看起来比以前好多了。您只使用了 Rich 的基本功能,但用户体验有了很大改善。
在这一步中,您对代码进行了几处重大修改。展开下面的部分,查看项目的完整源代码:
# wyrdl.py
import pathlib
import random
from string import ascii_letters
from rich.console import Console
from rich.theme import Theme
console = Console(width=40, theme=Theme({"warning": "red on yellow"}))
def main():
# Pre-process
words_path = pathlib.Path(__file__).parent / "wordlist.txt"
word = get_random_word(words_path.read_text(encoding="utf-8").split("\n"))
guesses = ["_" * 5] * 6
# Process (main loop)
for idx in range(6):
refresh_page(headline=f"Guess {idx + 1}")
show_guesses(guesses, word)
guesses[idx] = input("\nGuess word: ").upper()
if guesses[idx] == word:
break
# Post-process
game_over(guesses, word, guessed_correctly=guesses[idx] == word)
def refresh_page(headline):
console.clear()
console.rule(f"[bold blue]:leafy_green: {headline} :leafy_green:[/]\n")
def get_random_word(word_list):
words = [
word.upper()
for word in word_list
if len(word) == 5 and all(letter in ascii_letters for letter in word)
]
return random.choice(words)
def show_guesses(guesses, word):
for guess in guesses:
styled_guess = []
for letter, correct in zip(guess, word):
if letter == correct:
style = "bold white on green"
elif letter in word:
style = "bold white on yellow"
elif letter in ascii_letters:
style = "white on #666666"
else:
style = "dim"
styled_guess.append(f"[{style}]{letter}[/]")
console.print("".join(styled_guess), justify="center")
def game_over(guesses, word, guessed_correctly):
refresh_page(headline="Game Over")
show_guesses(guesses, word)
if guessed_correctly:
console.print(f"\n[bold white on green]Correct, the word is {word}[/]")
else:
console.print(f"\n[bold white on red]Sorry, the word was {word}[/]")
if __name__ == "__main__":
main()
现在,只要用户按照您的预期进行游戏,您的游戏就能很好地运行。试试如果你的猜测不是五个字母那么长会发生什么!下一步,您将添加一些反馈机制,以便在用户做错事情时给予指导。
由于篇幅较长,我就拆成上中下三篇Blog方便阅读。如有兴趣,可以持续关注我的动态哦!