4.5 案例研究:一个运气游戏
在本节中,我们模拟了一种流行的骰子游戏,称为“craps”。以下是需求声明:
投掷两个六面骰子,骰子每个面上的点数分别为1、2、3、4、5、6。当骰子停下来时,计算两个朝上的面上的点数总和。如果第一次掷骰的点数总和是7或11,则游戏胜利。如果第一次掷骰的点数总和为2、3或12(称为“craps”),游戏失败(即“house”获胜)。如果第一次掷骰的点数总和是4、5、6、8、9或10,那么这个总和就被记作“point”。想要获胜,必须继续掷骰子直到再次投出“point”。如果在得到“point”之前,出现了7,则游戏失败。
下面的脚本模拟了这个游戏并重复执行了几次,分别演示了游戏的四种结果:在第一次投掷时获胜;在第一次投掷时失败;在后续的投掷中获胜;在后续的投掷中失败。
函数roll_dice
—通过元组返回多个值
函数roll_dice
(第5~9行)用来模拟每次投掷两个骰子。该函数定义一次,随后(第16和33行)调用了两次。空的参数列表表示roll_dice
执行任务时不需要参数。
到目前为止,我们调用过的内置函数和自定义函数都只返回一个值。但有些时候需要返回多个值,例如函数roll_dice
,它将两个骰子的值组成一个元组返回(第9行)。元组是一个不可变(即不可修改)的值序列。要创建元组,可以使用逗号分隔其值,如第9行所示:
(die1, die2)
这个过程称为打包元组。括号是可选的,但为了清楚起见,建议使用它们。我们将在下一章深入讨论元组。
函数display_dice
要使用元组的值,可以将它们赋值给以逗号分隔的变量列表,称为解包元组。为了显示每次投掷骰子的结果,函数display_dice
(在第11~14行定义并在第17行和第34行中调用)对它接收的元组参数(第13行)进行了解包。“=
”左边的变量个数必须与元组中元素的个数相匹配,否则,会引发ValueError
。第14行打印一个包含两个骰子的点数及点数总和的格式化字符串。我们通过将元组传递给内置的sum
函数来计算骰子的总和。同列表一样,元组也是一个序列。
通过观察可以发现,函数roll_dice
和display_dice
都使用文档字符串作为函数块的开头,用来说明函数的功能。此外,两个函数都包含局部变量die1
和die2
,这些变量不会发生“冲突”,因为它们属于不同的函数块,而每个局部变量只在定义它的块中可访问。
第一次投掷
当脚本开始执行时,第16~17行投掷骰子并显示结果。第20行计算骰子点数的总和,并在第22~29行中使用这个值。第一次投掷以及任何一次后续的投掷都有赢或输的可能,变量game_status
用来跟踪输/赢的状态。
第22行
中的运算符in
用来测试元组(7,11)
是否包含sum_of_dice
的值。如果投出了7
或11
,此条件为True
。在这种情况下,第一次投掷就赢得游戏,因此脚本将game_status
设置为'WON'
。运算符“in
”的右操作数可以是任何可迭代的对象。此外,还可以使用“not in
”运算符来确定值是否不在可迭代对象中。上面的简明条件相当于
类似地,第24行中的条件
用来测试元组(2,3,12
)是否包含sum_of_dice
的值。如果包含,第一次投掷就输了,所以脚本将game_status
设置为'LOST'
。
对于骰子的任何其他点数总和(4、5、6、8、9或10),按照以下步骤进行处理:
- 第27行将
game_status
设置为'CONTINUE'
,继续投掷。 - 第28行将骰子点数的总和存储在
my_point
中,如果想在后续的投掷中获胜,必须再次投出my_point
的点数。 - 第29行显示
my_point
。
后续的投掷
如果game_status
等于'CONTINUE'
(第32行),则表示还不能确定输赢,因此执行while
语句的套件(第33~40行)。每次循环迭代都会调用函数roll_dice
来得到骰子的点数并计算它们的总和。如果sum_of_dice
等于my_point
(第37行)或7(第39行),则脚本分别将game_status
设置为'WON'
或'LOST'
,并且终止循环;否则,while
循环继续进行下一次投掷。
显示最终结果
当循环终止时,脚本运行到if...else
语句(第43~46行),如果game_status
为'WON'
,则输出'Player wins'
,否则输出'Player loses'
。