石头剪刀布看起来只是几个 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
程序会持续接受输入,直到玩家输入 q、quit 或 exit。输入清洗集中在 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
动手前的检查清单
实现这类命令行小游戏时,可以检查以下几点:
- 是否明确支持退出操作?
- 非法输入是否会得到提示,而不是引发异常?
- 随机选择是否只从合法选项中产生?
- 胜负规则是否集中管理,避免重复条件?
- 输入、规则判断、计分和输出是否分成独立函数?
- 是否为关键规则添加了最小测试?
石头剪刀布的代码量很小,但它把输入处理、循环、枚举、字典、随机数和测试串在了一起。把这些基础结构写清楚,比单纯让程序“能玩”更重要;这也是命令行工具逐步走向可维护代码的起点。