Shell 技巧 - 计算牌点
在我最近的几篇文章中,我们已经很好地尝试开始在 shell 的限制和能力范围内构建一个 二十一点 游戏。上一篇文章总结了如何洗一副 52 个整数值的数组,以及如何展开给定的牌来识别花色和点数,以便能够吸引人地显示出来。
这篇文章进一步深入探讨了 二十一点 的数学原理,介绍了一个例程,它可以接收一个纸牌数组并返回牌面点数值。如果您是 二十一点 玩家,您会立即注意到我们目前跳过了一些东西。在 二十一点 中,A 可以计为 1 点或 11 点,这就是为什么 A + K 的牌面可以是二十一点(即,价值 21 点)。
在本游戏的第一遍中,我们将 A 算作 11 点,也许稍后我们会回来添加 A 可以是 1 点或 11 点的细微差别。顺便说一句,请注意,这增加了相当大的复杂性,因为这样一来,A + A 的牌面就有四种计分方式(分别为 2、12、12 或 22),因此从理论上讲,返回给定牌面点数值的例程实际上应该返回一个数值数组。
但是,让我们从最直接的情况开始。上个月,我展示了如何使用以下等式提取给定牌的点数:
rank=$(( $card % 13 ))
以典型的 UNIX 方式,点数实际上范围从 0-12,而不是预期的 1-13,因此,因为我们希望将每种花色的牌 #2-10 保留为相应的数值,这意味着我们遇到了相当奇怪的情况,即点数 0 = K,点数 1 = A,点数 11 = J,点数 12 = Q。其实没关系,因为无论如何我们都必须将牌的点数映射为数值,无论我们怎么处理。
考虑到这一点,这是一个函数,可以将一组牌值转换为点数值,记住所有花牌都值 10 点,并且目前,A 只值 11 点
function handValue { # feed this as many cards as are in the hand handvalue=0 # initialize for cardvalue do rankvalue=$(( $cardvalue % 13 )) case $rankvalue in 0|11|12 ) rankvalue=10 ;; 1 ) rankvalue=11 ;; esac handvalue=$(( $handvalue + $rankvalue )) done }
在进一步深入之前,让我们研究一下其中的一些细微之处。首先,请注意,条件 case 语句可能非常复杂,因此我们使用简洁的 0|11|12 表示法捕获了 rankvalue = 0 (K)、rankvalue = 11 (J) 和 rankvalue = 12 (Q) 这三种情况。
我更喜欢这个函数的另一点是,通过使用没有指定循环约束的 for 循环,shell 会自动遍历传递给函数的所有值,然后终止,这意味着我们有一个很好的、灵活的函数,它可以很好地处理四张或五张牌的情况,就像处理两张牌一样。(事实证明,二十一点 手牌中不能超过五张牌,因为如果您拿到五张牌并且没有超过 21 点的点数值,您就拥有“五张牌蒙提”,这手牌相当不错!)
调用这个函数通常很麻烦,就像 shell 中的所有函数一样,因为您实际上无法返回值并将其分配给变量或将其包含在 echo 语句或类似语句中。以下是我们如何轻松计算玩家手牌和庄家手牌的初始点数值
handValue ${player[1]} ${player[2]} echo "Player's hand is worth $handvalue points" handValue ${dealer[1]} ${dealer[2]} echo "Dealer's hand is worth $handvalue points"
二十一点 是一种非常有利于庄家的游戏,因为玩家必须先拿牌并打完手牌,然后庄家才能拿一张牌。因此,庄家有明显的优势,但在这种情况下,我们现在可以有一个循环,询问玩家是否想要再拿一张牌(“要牌”)还是坚持他们手中的牌(“停牌”),只需跟踪他们的牌并在每次要牌后调用 handValue,以确保他们没有超过 21 点(“爆牌”)。
为了让它工作,我们必须重构一些代码(这在程序演变过程中并不少见)。现在我们不是简单地引用牌组本身,而是有两个数组,一个用于玩家,一个用于庄家。为了初始化它们,我们将值 -1 放入每个槽位(在初始化函数中)。然后,我们用以下方式发牌:
player[1]=${newdeck[1]} player[2]=${newdeck[3]} nextplayercard=3 # player starts with two cards dealer[1]=${newdeck[2]} dealer[2]=${newdeck[4]} nextdealercard=3 # dealer also has two cards nextcard=5 # we've dealt the first four cards already
您可以看到我们需要使用的跟踪变量,以记住我们在牌组中移动了多远。我们不想给两个玩家相同的牌!
考虑到这个循环,这是主要的玩家循环
while [ $handvalue -lt 22 ] do echo -n "H)it or S)tand? " read answer if [ $answer = "stand" -o $answer = "s" ] ; then break fi player[$nextplayercard]=${newdeck[$nextcard]} showCard ${player[$nextplayercard]} echo "** You've been dealt: $cardname" handValue ${player[1]} ${player[2]} ${player[3]} ${player[4]} ${player[5]} nextcard=$(( $nextcard + 1 )) nextplayercard=$(( $nextplayercard + 1 )) done
这是这个循环的简化版本。更复杂的版本可以在 Linux Journal FTP 站点上找到 (ftp.linuxjournal.com/pub/lj/listings/issue145/8860.tgz)。请注意,它非常简单明了。只要手牌点数小于 22 点,玩家就可以加牌或选择停牌。在后一种情况下,break 语句会将您从 while 循环中拉出来,准备继续执行程序。
因为 nextcard 是指向牌组的指针,用于跟踪已发出了多少张牌,所以每次发牌时都需要递增它,但是由于我们也在使用 nextplayercard 来跟踪单个玩家数组,所以我们也需要在每次循环时递增它。
但是,在结束之前,让我们看一下一个简单的调整。与其仅仅询问玩家是否想要要牌或停牌,我们可以通过计算手牌点数是否小于 16 来推荐一个行动
if [ $handvalue -lt 16 ] ; then echo -n "H)it or S)tand? (recommended: hit) " else echo -n "H)it or S)tand? (recommended: stand) " fi
一般来说,我们会有一个快速演示,但请注意,我们的脚本中确实存在一些错误需要首先处理,尽管如此
$ blackjack.sh ** You've been dealt: 3 of Clubs, Queen of Clubs H)it or S)tand? (recommended: hit) h ** You've been dealt: 8 of Hearts H)it or S)tand? (recommended: stand) s You stand with a hand value of 21
完美。这是另一次运行
$ blackjack.sh ** You've been dealt: 4 of Clubs, Jack of Hearts H)it or S)tand? (recommended: hit) h ** You've been dealt: 10 of Diamonds *** Busted! Your hand is worth 24 **
啊,最后一把运气不好!
与其指出具体问题,不如让我在此指出,拿到以下两个序列中的任何一个都是相当大的问题:A A 或 2 2 2 2 3 4。您能看出原因吗?
下个月,我们将研究解决这些问题!
Dave Taylor 是 UNIX 领域 26 年的老将,The Elm Mail System 的创建者,也是最近畅销书 Wicked Cool Shell Scripts 和 Teach Yourself Unix in 24 Hours 的作者,以及其他 16 本技术书籍的作者。他的主要网站是 www.intuitive.com。