Shell 技巧 - 计算牌点

作者:Dave Taylor

在我最近的几篇文章中,我们已经很好地尝试开始在 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 ScriptsTeach Yourself Unix in 24 Hours 的作者,以及其他 16 本技术书籍的作者。他的主要网站是 www.intuitive.com

加载 Disqus 评论