Python程序设计:人工智能案例实践
上QQ阅读APP看书,第一时间看更新

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_dicedisplay_dice都使用文档字符串作为函数块的开头,用来说明函数的功能。此外,两个函数都包含局部变量die1die2,这些变量不会发生“冲突”,因为它们属于不同的函数块,而每个局部变量只在定义它的块中可访问。

第一次投掷

当脚本开始执行时,第16~17行投掷骰子并显示结果。第20行计算骰子点数的总和,并在第22~29行中使用这个值。第一次投掷以及任何一次后续的投掷都有赢或输的可能,变量game_status用来跟踪输/赢的状态。

第22行

中的运算符in用来测试元组(7,11)是否包含sum_of_dice的值。如果投出了711,此条件为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'