单人纸牌(Elevens Lab)活动11:模拟Elevens牌局

陈 欣发布

导言

我们已经实现了两个不同的单人纸牌游戏,Elevens和Thirteens。如果只是想自娱自乐的化这已经足够了,但如果我们想回答关于这些游戏原理的问题呢?例如,Elevens游戏取胜的百分比是怎样的?你或许心里已经有些初步的想法,但要真正验证它们至少要玩几千局以上。这就是需要用到模拟(simulation)的地方了。

探索

对于一些过程的模拟是一种常见的计算应用,换言之,我们编写程序来在某些形式上模仿过程。程序的行为和状态体现了过程的关键特征,或者说是过程的一个模型(model)。比如我们可以设计一个扫地机的模拟。模拟程序内部会追踪所处的环境、电池余量和灰尘仓中的灰尘容量,并可能在命令行中输出这些数据。程序的method会规划机器人在“房间”的行动路径,并决定如何算作打扫完成。

对于一些在真实世界中观察起来非常复杂、缓慢、危险或者昂贵的过程而言,模拟是非常有用的手段。此外编写模拟程序也能帮助程序员理解被模拟的过程。例如,扫地机的生产厂商会利用模拟程序来在生产真机前调试其算法。

如果某个程序的状态改变受到概率影响,我们称其为概率性的(probabilistic)。交通模拟是一个例子,因为进入区域的机动车的时机和速度均是不确定的。更加明显的例子是基于骰子或轮盘的游戏模拟。在Elevens中,概率性元素是牌组的洗牌。

为给随机事件建模,我们使用伪随机数生成器(pseudo-random number generator,pseudo通常省略)。在Deckshuffle method中,我们使用Math.random method来生成随机数。这种对概率的重要依赖极大的复杂化了对模拟表现正确与否的验证。程序员自己需要对输出有一些先验的认识。此外,概率事件的一小部分结果可能会有误导性。例如,四次投掷硬币可能都得到正面,但如果觉得这是时常发生的事就大错特错了。一万余次掷硬币的话,正面和反面在大概率上是各占一半。典型的概率模拟需要大量调用随机数生成器以增加结果和期望行为一致的可能性。

为了模拟Elevens,我们需要使用程序状态和行为为“玩”游戏建模。让我们看看真实世界与代码是如何联系的:

状态:

真实世界  程序数据
牌组  Card object的列表
牌局  Card object的数组

行为:

真实世界  程序操作
寻找要移除的牌  在牌局中寻找特定的Card object组合(11对子或JQK)
移除并替换牌  将Card object从牌局中移除并替换成牌组发出的Card object
发牌 从牌组中删去一个Card object

大部分的代码已经写好了。我们已经解决了所有的对于状态的要求。牌组已经被class Deckcards列表所建模。桌面上的卡片则由class Boardcards数组表示。我们也为大多数必要的行为编写了相应的method。事实上,我们只需要为你自己玩这个游戏的额外操作建模。

在本练习中,你需要使用class ElevensSimulation来进行Elevens游戏。它将会需要使用模仿你玩游戏时那些行动的method。此时你到底会做些什么,又为什么做这些事呢?回答以下问题:

  • 你玩游戏时常重复的事情是什么?
  • 在你审视桌面上的牌时,你在试图找到什么?
  • 为什么你选择某组牌?
  • 当你点击Replace按钮时发生了什么?

我们将用class ElevensBoard中的三个新method来为这些行为建模:

  • playIfPossible——寻找合适的牌组合并换牌(如果找到的话)。这是ElevensSimulation需要直接调用的唯一新method。我们可以把我们的所有新代码放到这个method中,但是把它分成两个新的private助手method会更方便些。
  • playPairSum11IfPossible——寻找11对子并换牌(如果找到的话)。
  • playJQKIfPossible——寻找JQK并换牌(如果找到的话)。

接下来我们考虑playPairSum11IfPossible的实现。这一方法需要先确定桌面上的牌局中是否包含和为11的对子。然后如果找到的话,它需要能将对子移除。当然,playPairSum11IfPossible可以调用containsPairSum11来查看桌面上是否有11对子。不过,containsPairSum11并不返回11对子索引的任何信息,因而playPairSum11IfPossible在移牌之前还需要再找一次。为了避免两次找牌的操作,我们固然可以把containsPairSum11的代码直接复制到playPairSum11IfPossible里。不过好在我们有更好的选择。

我们可以把containsPairSum11 method改成findPairSum11 method。换言之,在用返回boolean值的“包含”method之外,我们还可以有一个“搜索”method,返回找到的一对牌的索引。如果牌局中没有11对子,method可以返回一个空列表。这样,playPairSum11IfPossible就能调用findPairSum11。当牌局中有11对子时,模拟method就可以得到对子的索引并将其作为调用replaceSelectedCards method的参数。这种设计避免了重复代码和重复劳动!对于containsJQK method,我们可以进行类似的改动,让playJQKIfPossible method可以调用findJQK method。

注意在程序设计的初始阶段,我们很难预料到方方面面。例如,Elevens的GUI版用不到“搜索”method,因为这是玩家所做的事。然而,在我们涉及到模拟细节时,“搜索”method就变得有用起来了。因此程序设计是可以改变的,也是会变化的。

当然,在我们最初进行程序设计时,我们会试图按照自己的理解来满足所有需求。但我们也会在设计中为未来的需要留有余地。程序设计的准则之一是尽可能用private method,除非其他的class有足够好的理由要调用这些method。因为我们最初将containsPairSum11设为private,我们就知道没有其他的class使用它。这样,就可以很方便的修改和重命名,没有太多顾虑。

练习

  1. 首先,仔细阅读目录里完成的class ElevensSimulation。这个模拟创建一个ElevensBoard object,利用它来玩GAMES_TO_PLAY局Elevens游戏。注意playIfPossible仅调用了ElevensBoard中的新method。
  2. 现在对class ElevensBoard做必要的修改。将containsPairSum11改为findPairSum11。method签名和实现都需要改动。注意注释的部分已经为你改好了。
  3. 修改isLegalanotherPlayIsPossible method,让它们调用修改后的findPairSum11。注意当且仅当findPairSum11(cIndexes).size() > 0时牌局中才有11对子。
  4. 按照类似的方式将containsJQK改为findJQK。注释的部分已经为你改好了。
  5. 修改isLegalanotherPlayIsPossible method,让它们调用修改后的findJQK。这时,GUI版Elevens应该像原来一样工作。
  6. 现在该完成ElevensSimulation所需的ElevensBoard method了。首先完成class ElevensSimulation所调用的public playIfPossible method。该method将使用playPairSum11IfPossibleplayJQKIfPossible这两个private助手method。注意你需要将return语句替换掉。
  7. 完成playPairSum11IfPossibleplayJQKIfPossible这两个private助手method。注意你需要将return语句替换掉。
  8. 现在是时候测试你为进行模拟而做的改动了。确保ElevensSimulation中的GAMES_TO_PLAYI_AM_DEBUGGING均被分别初始化为1true。再确保ElevensBoard中的I_AM_DEBUGGING初始化为true。运行几次ElevensSimulation程序并检查输出。你应该能看到牌局中的11对子和JQK均被正确识别和移除。

问题

  1. I_AM_DEBUGGING标记设为falseGAMES_TO_PLAY设为10。运行几次ElevensSimulation程序并记录每次运行中游戏获胜的百分比。你所看到的百分比在什么范围内?数据是比较一致还是彼此间有相当的差异?
  2. 将模拟局数改为100。每次模拟的获胜百分比是不是更加一致?
  3. 再尝试其他的模拟局数。每次需要模拟多少局能得到比较一致的结果。
  4. (可选)为Thirteens游戏重复以上步骤。

陈 欣

AADPS创始人

0 条评论

发表回复