【Dison夏令营 Day 06】用 Python 和 Rich 制作 Wordle克隆(中篇)

在大流行期间,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方便阅读。如有兴趣,可以持续关注我的动态哦!

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值