用 Python 做一个命令行石头剪刀布:从循环到规则表

2026-08-28 30 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

石头剪刀布看起来只是几个 if,却很适合用来检验 Python 命令行程序的基本功:如何读取玩家输入、控制游戏循环、随机生成电脑选择,以及用枚举和字典表达胜负规则。下面从一个可运行的小程序出发,再拆解实现中的关键决策。

先把游戏规则变成代码

游戏只有三种选择:石头、剪刀和布。与其写一长串嵌套条件,不如把“某个选择能击败什么”直接放进字典:

BEATS = {
    "rock": "scissors",
    "scissors": "paper",
    "paper": "rock",
}

这样判断胜负时只需要检查:BEATS[player] == computer。规则数据和流程代码分开后,程序更容易阅读,也更容易扩展。

如果希望避免到处散落字符串,可以使用 Enum 表示合法选项:

from enum import Enum


class Move(Enum):
    ROCK = "rock"
    PAPER = "paper"
    SCISSORS = "scissors"

Enum 的价值不只是让代码更“正式”。它可以集中定义允许的值,减少拼写错误,也能让后续重构更加安全。

一个可以直接运行的命令行版本

把下面内容保存为 rps.py,使用 Python 3 运行:

from enum import Enum
import random


class Move(Enum):
    ROCK = "rock"
    PAPER = "paper"
    SCISSORS = "scissors"


BEATS = {
    Move.ROCK: Move.SCISSORS,
    Move.SCISSORS: Move.PAPER,
    Move.PAPER: Move.ROCK,
}

ALIASES = {
    "rock": Move.ROCK,
    "r": Move.ROCK,
    "paper": Move.PAPER,
    "p": Move.PAPER,
    "scissors": Move.SCISSORS,
    "s": Move.SCISSORS,
}


def read_player_move():
    while True:
        value = input("Choose rock, paper, or scissors (q to quit): ").strip().lower()

        if value in {"q", "quit", "exit"}:
            return None

        if value in ALIASES:
            return ALIASES[value]

        print("Invalid choice. Please enter rock, paper, scissors, or q.")


def winner(player, computer):
    if player == computer:
        return "tie"
    if BEATS[player] == computer:
        return "player"
    return "computer"


def main():
    score = {"player": 0, "computer": 0, "tie": 0}
    moves = list(Move)

    print("Rock, Paper, Scissors")

    while True:
        player = read_player_move()
        if player is None:
            break

        computer = random.choice(moves)
        result = winner(player, computer)
        score[result] += 1

        print(f"You: {player.value}")
        print(f"Computer: {computer.value}")
        print(f"Result: {result}")
        print(f"Score: {score}\n")

    print("Thanks for playing!")
    print(f"Final score: {score}")


if __name__ == "__main__":
    main()

运行命令:

python rps.py

程序会持续接受输入,直到玩家输入 qquitexit。输入清洗集中在 read_player_move() 中,因此主循环不需要处理大小写、空格和快捷键等细节。

这道题真正考查什么

输入验证与循环

命令行程序不能假设用户永远输入合法值。while True 配合显式退出条件,可以让非法输入回到提示阶段,而不是直接让程序崩溃。

这里使用了:

value = input(...).strip().lower()

strip() 去掉首尾空格,lower() 统一大小写。随后通过 ALIASES 判断输入是否有效。若未来增加数字选项,例如 1 表示石头,只需扩展映射表,不必改动游戏主流程。

随机选择

电脑的选择来自:

computer = random.choice(list(Move))

list(Move) 会得到三个枚举成员,random.choice() 从中随机抽取一个。对于这个小游戏,标准库的伪随机数已经足够;如果是在安全敏感场景生成不可预测结果,则不能把它当作密码学随机方案。

规则判断

胜负函数可以写成三个分支,但字典方式更紧凑:

def winner(player, computer):
    if player == computer:
        return "tie"
    if BEATS[player] == computer:
        return "player"
    return "computer"

函数只负责比较两个选择,不负责读取输入、打印结果或修改分数。这种拆分让它容易测试:给定两个 Move,结果应当始终一致。

可以怎样继续改造

如果要把这个练习做成更完整的命令行工具,可以沿着几个方向扩展:

  • 增加“先赢三局”的目标,而不是无限进行。
  • 使用 argparse 支持 --rounds--seed 等命令行参数。
  • 将分数保存到 JSON 文件,下一次运行时继续查看记录。
  • 添加单元测试,覆盖平局、玩家胜利、电脑胜利和非法输入。
  • BEATS 改成更通用的规则配置,以支持“石头、剪刀、布、蜥蜴、斯波克”等变体。

一个简单的测试示例可以直接运行:

# test_rps.py
from rps import Move, winner


def test_winner():
    assert winner(Move.ROCK, Move.SCISSORS) == "player"
    assert winner(Move.PAPER, Move.PAPER) == "tie"
    assert winner(Move.SCISSORS, Move.ROCK) == "computer"

如果使用 pytest,安装并运行:

python -m pip install pytest
pytest -q

动手前的检查清单

实现这类命令行小游戏时,可以检查以下几点:

  • 是否明确支持退出操作?
  • 非法输入是否会得到提示,而不是引发异常?
  • 随机选择是否只从合法选项中产生?
  • 胜负规则是否集中管理,避免重复条件?
  • 输入、规则判断、计分和输出是否分成独立函数?
  • 是否为关键规则添加了最小测试?

石头剪刀布的代码量很小,但它把输入处理、循环、枚举、字典、随机数和测试串在了一起。把这些基础结构写清楚,比单纯让程序“能玩”更重要;这也是命令行工具逐步走向可维护代码的起点。


相关推荐