作者 | 云朵君
最近有粉丝朋友聊到用Python做个石头剪刀布的小游戏。我一寻思,还挺好玩。其实游戏编程是学习如何编程的一个好方法,它会使用许多我们在现实世界中看到的工具,还可以玩一个游戏来测试我们的编程结果!作为Python游戏编程的入门游戏:石头剪刀布,我们今天就来一起玩一玩。
在文中我们将一起学习如何:
- 为剪刀石头布游戏码代码
- 用
input()
- 使用
while
循环连续玩几个游戏 - 用
Enum
和函数简化代码 - 用字典定义更复杂的规则
什么是石头剪刀布?
大家以前都玩过石头剪刀布吧。假装你不熟悉,石头剪刀布是一个供两个或更多人玩的手部游戏。参与者说 "石头、剪刀、布"
,然后同时将他们的手捏成石头(拳头)、一张布(手掌朝上)或一把剪刀(伸出两个手指)的形状。
规则是直截了当的:
- 石头砸剪刀。
- 布包石头。
- 剪刀剪布。
现在用了这些规则,可以开始考虑如何将它们转化为Python代码。
在Python中玩单一的石头剪刀布游戏
使用上面的描述和规则,我们可以做一个石头剪刀布的游戏。首先需要导入用来模拟计算机选择的模块。
import random
现在我们能够使用随机里面的不同工具来随机化计算机在游戏中的行动。由于我们的用户也需要能够选择行动,所以需要接受用户的输入。
接收用户输入
从用户那里获取输入信息在Python中是非常直接的。这里的目标是问用户他们想选择什么行动,然后把这个选择分配给一个变量。
user_action = input("输入一个选择(石头、剪刀、布):")
这将提示用户输入一个选择,并将其保存在一个变量中供以后使用。用户已经选择了一个行动后,轮到计算机决定做些什么。
计算机选择
竞争性的石头剪刀布游戏涉及策略
还正有人研究并把石头剪刀布游戏策略写成学术论文,感兴趣的小伙伴可以查看论文(传送门:https://arxiv.org/pdf/1404.5199v1.pdf)
研究人员将 360 名学生分成六人一组,让他们随机配对玩 300 轮石头剪刀布。学生们每次赢得一轮比赛都会得到少量的钱。在他们玩游戏的过程中,研究人员观察了玩家在输赢时如何在三个游戏选项中轮换。
他们发现,“如果一名玩家在一场比赛中战胜对手,她在下一场比赛中重复相同动作的概率大大高于她改变动作的概率。” 如果一名玩家输了两次或两次以上,她很可能会改变她的打法,并且更有可能转向能够击败刚刚击败她的对手而不是她的对手刚刚击败她的动作。例如,如果小红对小明的石头玩剪刀输了,小红最有可能改用纸,这会打败小明的石头。根据研究,这是一个合理的策略,因为小明很可能会继续玩已经获胜的动作。作者将此称为“赢-留,输-转变”策略。
因此,这是在剪刀石头布上获胜的最佳方法:如果你输掉了第一轮,切换到击败对手刚刚玩过的动作。如果你赢了,不要继续玩同样的动作,而是换成能打败你刚刚玩的动作的动作。换句话说,玩你失败的对手刚刚玩的动作。也就是说:你用石头赢了一轮别人的剪刀,他们即将改用布,此时你应该改用剪刀。
根据上述的游戏策略,试图开发一个模型,应该需要花费不少的时间。为了简便,我们让计算机选择一个随机的行动来节省一些时间。随机选择就是让计算机选择一个伪随机值。
可以使用 random.choice()
possible_actions = ["石头", "剪刀", "布"]
computer_action = random.choice(possible_actions)
这允许从列表中选择一个随机元素。我们也可以打印出用户和计算机的选择。
print(f"\n你选择了 {user_action},
计算机选择了 {computer_action}.\n")
打印输出用户和计算机的操作对用户来说是有帮助的,而且还可以帮助我们在以后的调试中,以防结果不大对劲。
判断输赢
现在,两个玩家都做出了选择,我们只需要使用if ... elif ... else 代码块
方法来决定谁输谁赢,接下来比较玩家的选择并决定赢家。
if user_action == computer_action:
print(f"两个玩家都选择了 {user_action}. 这是一个平局!")
elif user_action == "石头":
if computer_action == "剪刀":
print("石头砸碎剪刀!你赢了!")
else:
print("布包住石头!你输了。")
elif user_action == "布":
if computer_action == "石头":
print("布包住石头!你赢了!")
else:
print("剪刀剪碎布!你输了。")
elif user_action == "剪刀":
if computer_action == "布":
print("剪刀剪碎布!你赢了!")
else:
print("石头砸碎剪刀!你输了。")
通过先比较平局条件,我们摆脱了相当多的情况。否则我们就需要检查 user_action
的每一个可能的动作,并与 computer_action
的每一个可能的动作进行比较。通过先检查平局条件,我们能够知道计算机选择了什么,只需对 computer_action
进行两次条件检查。
所以完整代码现在应该是这样的:
上下滑动查看更多源码
import random
user_action = input("输入一个选择(石头、剪刀、布):")
possible_actions = ["石头", "剪刀", "布"]
computer_action = random.choice(possible_actions)
print(f"\n你选择了 {user_action}, 计算机选择了 {computer_action}.\n")
if user_action == computer_action:
print(f"两个玩家都选择了 {user_action}. 这是一个平局!")
elif user_action == "石头":
if computer_action == "剪刀":
print("石头砸碎剪刀!你赢了!")
else:
print("布包住石头!你输了。")
elif user_action == "布":
if computer_action == "石头":
print("布包住石头!你赢了!")
else:
print("剪刀剪碎布!你输了。")
elif user_action == "剪刀":
if computer_action == "布":
print("剪刀剪碎布!你赢了!")
else:
print("石头砸碎剪刀!你输了。")
现在我们已经写好了代码,可以接受用户的输入,并为计算机选择一个随机动作,最后决定胜负!这个初级代码只能让我们和电脑玩一局。
连续打几场比赛
虽然单一的剪刀石头布游戏比较有趣,但如果我们能连续玩几场,不是更好吗?此时我们想到 循环 是创建重复性事件的一个好方法。我们可以用一个 while循环 来无限期地玩这个游戏。
import random
while True:
# 包住上完整代码
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break
注意我们补充的代码,检查用户是否想再玩一次,如果他们不想玩就中断,这一点很重要。如果没有这个检查,用户就会被迫玩下去,直到他们用Ctrl+C
或其他的方法强制终止程序。
对再次播放的检查是对字符串 "y"
的检查。但是,像这样检查特定的东西可能会使用户更难停止游戏。如果用户输入 "yes"
或 "no"
怎么办?字符串比较往往很棘手,因为我们永远不知道用户可能输入什么。他们可能会做所有的小写字母,所有的大写字母,甚至是输入中文。
下面是几个不同的字符串比较的结果。
>>> play_again = "yes"
>>> play_again == "n"
False
>>> play_again != "y"
True
其实这不是我们想要的。如果用户输入 "yes"
,期望再次游戏,却被踢出游戏,他们可能不会太高兴。
enum.IntEnum描述动作
我们在之前的示意代码中,定义的是中文字符串,但实际使用python开发时,代码里一般不使用中文(除了注释),因此了解这一节还是很有必要的。
所以我们将把石头剪刀布翻译成:"rock", "scissors", "paper"
。
字符串比较可能导致像我们上面看到的问题,所以需要尽可能避免。然而,我们的程序要求的第一件事就是让用户输入一个字符串!如果用户错误地输入了 "Rock "或 "rOck "怎么办?如果用户错误地输入 "Rock "或 "rOck "怎么办?大写字母很重要,所以它们不会相等。
>>> print("rock" == "Rock")
False
由于大写字母很重要,所以 "r"
和 "R"
并不相等。一个可能的解决方案是用数字代替。给每个动作分配一个数字可以为我们省去一些麻烦。
ROCK_ACTION = 0
SCISSORS_ACTION = 1
PAPER_ACTION = 2
我们通过分配的数字来引用不同的行动,整数不存在与字符串一样的比较问题,所以这是可行的。现在让用户输入一个数字,并直接与这些值进行比较。
user_input = input("输入您的选择 (石头[0], 剪刀[1], 布[2]): ")
user_action = int(user_input)
if user_action == ROCK_ACTION:
# 处理 ROCK_ACTION
因为input()
返回一个字符串,需要用int()
把返回值转换成一个整数。然后可以将输入值与上面的每个动作进行比较。虽然这样做效果很好,但它可能依赖于对变量的正确命名。其实有一个更好的方法是使用**enum.IntEnum
**来自定义动作类。
我们使用 enum.IntEnum
创建属性并给它们分配类似于上面所示的值,将动作归入它们自己的命名空间,使代码更有表现力。
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
这创建了一个自定义Action
,可以用它来引用我们支持的不同类型的Action
。它的工作原理是将其中的每个属性分配给我们指定的值。
两个动作的比较是直截了当的,现在它们有一个有用的类名与之相关。
>>> Action.Rock == Action.Rock
True
因为成员的值是相同的,所以比较的结果是相等的。类的名称也使我们想比较两个动作的意思更加明显。
注意:要了解更多关于enum的信息,请查看官方文档[1]。
我们甚至可以从一个 int
创建一个 Action
。
>>> Action.Rock == Action(0)
True
>>> Action(0)
<Action.Rock: 0>
Action 查看传入的值并返回适当的 Action。因此现在可以把用户的输入作为一个int
,并从中创建一个Action,妈妈再也不用担心拼写问题了!
程序流程(图)
虽然剪刀石头布看起来并不复杂,但仔细考虑玩剪刀石头布的步骤是很重要的,这样才能确保我们的程序涵盖所有可能的情况。对于任何项目,即使是小项目,我们有必要创建一个所需行为的流程图并围绕它实现代码。我们可以用一个列表来达到类似的效果,但它更难捕捉到诸如循环和条件等相关逻辑。
流程图不需要过于复杂,甚至不需要使用真正的代码。只要提前描述所需的行为,就可以帮助我们在问题发生之前解决问题
这里有一个流程图,描述了一个单一的剪刀石头布游戏。
每个玩家选择一个行动,然后确定一个赢家。这个流程图对于我们所编码的单个游戏来说是准确的,但对于现实生活中的游戏来说却不一定准确。在现实生活中,玩家会同时选择他们的行动,而不是像流程图中建议的那样一次一个。
然而,在编码版本中,这一点是可行的,因为玩家的选择对电脑是隐藏的,而电脑的选择对玩家也是隐藏的。两个玩家可以在不同的时间做出选择而不影响游戏的公平性。
流程图可以帮助我们在早期发现可能的错误,也可以让我们看到是否要增加更多的功能。例如这个流程图,描述了如何重复玩游戏,直到用户决定停止。
如果不写代码,我们可以看到第一个流程图没有办法重复玩。我们可以使用这种绘制流程图的方法在编程前解决类似的问题,这有助于我们码出更整洁、更易于管理的代码!
拆分代码并封装函数
现在我已经用流程图概述了程序的流程,我们可以试着组织我们的代码,使它更接近于所确定的步骤。一个方法是为流程图中的每个步骤创建一个函数。 其实函数是一种很好的方法,可以将大块的代码拆分成更小的、更容易管理的部分。
我们不一定需要为条件检查的再次播放创建一个函数,但如果我们愿意,我们可以。如果我们还没有,我们可以从导入随机开始,并定义我们的Action类。
import random
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
接下来定义 get_user_selection()
的代码,它不接受任何参数并返回一个 Action
。
def get_user_selection():
user_input = input("输入您的选择 (石头[0], 剪刀[1], 布[2]):")
selection = int(user_input)
action = Action(selection)
return action
注意这里是如何将用户的输入作为一个 int
,然后得到一个 Action
。不过,给用户的那条长信息有点麻烦。如果我们想增加更多的动作,就不得不在提示中添加更多的文字。
我们可以使用一个列表推导式来生成一部分输入。
def get_user_selection():
choices = [f"{action.name}[{action.value}]" for action in Action]
choices_str = ", ".join(choices)
selection = int(input(f"输出您的选择 ({choices_str}): "))
action = Action(selection)
return action
现在不再需要担心将来添加或删除动作的问题了!接下来测试一下,我们可以看到代码是如何提示用户并返回一个与用户输入值相关的动作。
>>> get_user_selection()
输入您的选择 (石头[0], 剪刀[1], 布[2]): 0
<Action.Rock: 0>
现在我们需要一个函数来获取计算机的动作。和 get_user_selection()
一样,这个函数应该不需要参数,并返回一个 Action
。因为 Action
的值范围是0到2
,所以使用 random.randint()
random.randint()
返回一个在指定的最小值和最大值(包括)之间的随机值。可以使用 len()
来帮助计算代码中的上限应该是多少。
def get_computer_selection():
selection = random.randint(0, len(Action) - 1)
action = Action(selection)
return action
因为 Action
的值从0开始计算,而len()
从1开始计算,所以需要额外做个 len(Action)-1
。
测试该函数,它简单地返回与随机数相关的动作。
>>> get_computer_selection()
<Action.Scissors: 2>
看起来还不错!接下来,需要一个函数来决定输赢,这个函数将接受两个参数,用户的行动和计算机的行动。它只需要将结果显示在控制台上,而不需要返回任何东西。
def determine_winner(user_action, computer_action):
if user_action == computer_action:
print(f"两个玩家都选择了 {user_action.name}. 这是一个平局!")
elif user_action == Action.Rock:
if computer_action == Action.Scissors:
print("石头砸碎剪刀!你赢了!")
else:
print("布包住石头!你输了。")
elif user_action == Action.Paper:
if computer_action == Action.Rock:
print("布包住石头!你赢了!")
else:
print("剪刀剪碎布!你输了。")
elif user_action == Action.Scissors:
if computer_action == Action.Paper:
print("剪刀剪碎布!你赢了!")
else:
print("石头砸碎剪刀!你输了。")
这里决定胜负的写法与刚开始的代码非常相似。而现在可以直接比较行动类型,而不必担心那些讨厌的字符串!
我们甚至可以通过向 determinal_winner()
传递不同的参数来测试函数,看看会打印出什么。
>>> determine_winner(Action.Rock, Action.Scissors)
石头砸碎剪刀!你赢了!
既然我们要从一个数字创建一个动作,如果用户想用数字3创建一个动作,会发生什么?(我们定义的最大数字是2)。
>>> Action(3)
ValueError: 3 is not a valid Action
报错了!这并不是我们希望发生这种情况。接下来可以在流程图上添加一些逻辑,来补充这个 bug,以确保用户始终输入一个有效的选择。
在用户做出选择后立即加入检查是有意义的。
如果用户输入了一个无效的值,那么我们就重复这个步骤来获得用户的选择。对用户选择的唯一真正要求是它在【0, 1, 2】之间的一个数。如果用户的输入超出了这个范围,那么就会产生一个ValueError异常。我们可以处理这个异常,从而不会向用户显示默认的错误信息。
现在我们已经定义了一些反映流程图中的步骤的函数,我们的游戏逻辑就更有条理和紧凑了。这就是我们的while循环现在需要包含的所有内容。
while True:
try:
user_action = get_user_selection()
except ValueError as e:
range_str = f"[0, {len(Action) - 1}]"
print(f"Invalid selection. Enter a value in range {range_str}")
continue
computer_action = get_computer_selection()
determine_winner(user_action, computer_action)
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break
这看起来是不是干净多了?注意,如果用户未能选择一个有效的范围,那么我们就使用continue
而不是break
。这使得代码继续到循环的下一个迭代,而不是跳出该循环。
Rock Paper Scissors … Lizard Spock
如果我们看过《生活大爆炸》,那么我们可能对石头剪子布蜥蜴斯波克很熟悉。如果没有,那么这里有一张图,描述了这个游戏和决定胜负的规则。
我们可以使用我们在上面学到的同样的工具来实现这个游戏。例如,我们可以在Action中加入Lizard
和Spock
的值。然后我们只需要修改 get_user_selection()
和 get_computer_selection()
,以纳入这些选项。然而,更新determinal_winner()
。
与其在我们的代码中加入大量的if ... elif ... else
语句,我们可以使用字典来帮助显示动作之间的关系。字典是显示 键值关系 的一个好方法。在这种情况下,键 可以是一个动作,如剪刀,而 值 可以是一个它所击败的动作的列表。
那么,对于只有三个选项的 determinal_winner()
来说,这将是什么样子呢?好吧,每个 Action
只能打败一个其他的 Action
,所以列表中只包含一个项目。下面是我们的代码之前的样子。
def determine_winner(user_action, computer_action):
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif user_action == Action.Rock:
if computer_action == Action.Scissors:
print("Rock smashes scissors! You win!")
else:
print("Paper covers rock! You lose.")
elif user_action == Action.Paper:
if computer_action == Action.Rock:
print("Paper covers rock! You win!")
else:
print("Scissors cuts cpaper! You lose.")
elif user_action == Action.Scissors:
if computer_action == Action.Paper:
print("Scissors cuts cpaper! You win!")
else:
print("Rock smashes scissors! You lose.")
现在,我们可以有一个描述胜利条件的字典,而不是与每个行动相比较。
def determine_winner(user_action, computer_action):
victories = {
Action.Rock: [Action.Scissors], # Rock beats scissors
Action.Paper: [Action.Rock], # Paper beats rock
Action.Scissors: [Action.Paper] # Scissors beats cpaper
}
defeats = victories[user_action]
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif computer_action in defeats:
print(f"{user_action.name} beats {computer_action.name}! You win!")
else:
print(f"{computer_action.name} beats {user_action.name}! You lose.")
我们还是和以前一样,先检查平局条件。但我们不是比较每一个 Action
,而是比较用户输入的 Action
与电脑输入的 Action
。由于键值对是一个列表,我们可以使用成员运算符 in
来检查一个元素是否在其中。
由于我们不再使用冗长的if ... elif ... else
语句,为这些新的动作添加检查是相对容易的。我们可以先把Lizard
和Spock
加入到Action中。
class Action(IntEnum):
Rock = 0
Scissors = 1
Paper = 2
Lizard = 3
Spock = 4
接下来,从图中添加所有的胜利关系。
victories = {
Action.Scissors: [Action.Lizard, Action.Paper],
Action.Paper: [Action.Spock, Action.Rock],
Action.Rock: [Action.Lizard, Action.Scissors],
Action.Lizard: [Action.Spock, Action.Paper],
Action.Spock: [Action.Scissors, Action.Rock]
}
注意,现在每个 Action
都有一个包含可以击败的两个元素的列表。而在基本的 "剪刀石头布 "
实现中,只有一个元素。
我们写了 get_user_selection()
来适应新的动作,所以不需要改变该代码的任何内容。get_computer_selection()
的情况也是如此。由于 Action
的长度发生了变化,随机数的范围也将发生变化。
看看现在的代码有多简洁,有多容易维护管理!完整程序的完整代码:
上下滑动查看更多源码
import random
from enum import IntEnum
class Action(IntEnum):
Rock = 0
Paper = 1
Scissors = 2
Lizard = 3
Spock = 4
victories = {
Action.Scissors: [Action.Lizard, Action.Paper],
Action.Paper: [Action.Spock, Action.Rock],
Action.Rock: [Action.Lizard, Action.Scissors],
Action.Lizard: [Action.Spock, Action.Paper],
Action.Spock: [Action.Scissors, Action.Rock]
}
def get_user_selection():
choices = [f"{action.name}[{action.value}]" for action in Action]
choices_str = ", ".join(choices)
selection = int(input(f"Enter a choice ({choices_str}): "))
action = Action(selection)
return action
def get_computer_selection():
selection = random.randint(0, len(Action) - 1)
action = Action(selection)
return action
def determine_winner(user_action, computer_action):
defeats = victories[user_action]
if user_action == computer_action:
print(f"Both players selected {user_action.name}. It's a tie!")
elif computer_action in defeats:
print(f"{user_action.name} beats {computer_action.name}! You win!")
else:
print(f"{computer_action.name} beats {user_action.name}! You lose.")
while True:
try:
user_action = get_user_selection()
except ValueError as e:
range_str = f"[0, {len(Action) - 1}]"
print(f"Invalid selection. Enter a value in range {range_str}")
continue
computer_action = get_computer_selection()
determine_winner(user_action, computer_action)
play_again = input("Play again? (y/n): ")
if play_again.lower() != "y":
break
到这里我们已经用Python代码实现了rock paper scissors lizard Spock
。接下来你就可以仔细检查一下,确保我们没有遗漏任何东西,然后进行一次游戏。
总结
看到这里,必须点个赞,因为我们刚刚完成了第一个Python游戏。现在,我们知道了如何从头开始创建剪刀石头布游戏,而且我可以以最小的代价扩展游戏中可能的行动数量。
参考资料
[1]