算牌:二十一点

作者:Dave Taylor

在过去的几个月里,我回顾了 shell 脚本编程的基础知识,所以我想现在是时候回到一个有趣的项目中了。用 shell 脚本捕捉游戏逻辑总是一个很好的挑战,特别是考虑到我们经常在 Bash shell 的功能方面进行探索。

对于这个新项目,让我们用脚本模拟一副纸牌的工作方式,并在进行过程中开发特定的函数。我们将从一个名为二十一点的两人或三人纸牌游戏开始。我们创建的基本函数也很容易扩展到简单的扑克变体和其他多牌评估问题。

如果您不熟悉二十一点,您有时间了解更多关于这个游戏的信息,因为我实际上要到下个月才会涉及任何特定于游戏的元素。需要一个学习的好地方?试试这个:http://www.bicyclecards.com/card-games/rule/cribbage

任何纸牌游戏的第一个也是最明显的挑战是模拟一副纸牌。然而,不仅仅是纸牌,洗牌也是一个挑战。您是否需要多次洗牌以使结果随机化?幸运的是,这没有必要,因为您可以按顺序创建一副牌(作为整数值的数组),并从牌堆中随机抽取牌,而不是担心洗牌并按顺序抽取。

这实际上完全是关于数组的,在 shell 脚本中,数组很容易使用:只需在数组中指定所需的索引,它就会被分配为一个有效的槽位。例如,我可以简单地使用 deck[52]=1,牌堆数组将创建槽位 0..52(尽管所有其他元素都将具有未定义的值)。

因此,创建有序的纸牌堆非常容易


for i in {0..51}
do
  deck[$i]=$i
done

由于我们将使用值 -1 来表示该牌已从牌堆中抽出,因此如果所有内容都设置为 -1 以外的任何值,这将同样有效,但我喜欢 deck[$i]=$i 的对称性。

还要注意我们正在使用的高级 for 循环。早期版本的 Bash 无法使用 {x..y} 表示法,因此如果失败,我们将需要手动递增变量。这不是一个大麻烦,但希望这可以正常工作。

为了抽牌,让我们利用神奇的 $RANDOM 变量,这是一个每次引用它时都有不同值的变量——非常方便,真的。

因此,从牌堆中随机抽取一张牌就像这样简单


card=${deck[$RANDOM % 52]}

请注意,为了避免不正确的语法分析,始终最好将数组引用为 ${deck[$x]} 而不是更简洁的 $deck[$x]

您如何知道是否已经从牌堆中抽出了特定的牌?我不在乎您玩什么游戏,像 3H、4D、5D、9H、9H 和 9H 这样的牌肯定会让您陷入麻烦!为了解决这个问题,我们将使用的算法如下


pick a card
if it's already been picked before
   pick again
until we get a valid card

在编程上,记住值 -1 表示已从牌堆中抽出的牌,它看起来像这样


until [ $card -ne -1 ]
do
  card=${deck[$RANDOM % 52]}
done
echo "Picked card $card from the deck"

抽出的第一张牌不是问题,但是如果您想发 52 张牌中的 45 张,那么当您拿到最后几张牌时,程序很可能会来回跳转,重复选择已经发出的牌,可能多达六次或更多次。在您要发出整副牌或重要子集的情况下,更智能的算法是计算您进行了多少次随机尝试,当您达到阈值时,然后从随机点开始按顺序浏览牌堆,直到找到可用的牌——以防万一随机数生成器不像我们希望的那么随机。

上面片段中缺少的部分是额外的代码片段,它将给定的牌标记为已被抽出,以便该算法识别出重复抽出的牌。我将添加它,添加一个我要发的六张牌的数组,并添加一个变量来跟踪所选特定牌的数组索引值


for card in {0..5} ; do
  until [ ${hand[$card]} -ne -1 ]
  do
    pick=$(( $RANDOM % 52 ))
    hand[$card]=${deck[$pick]}
  done
  echo "Card ${card} = ${hand[$card]}"
  deck[$pick]=-1     # no longer available
done

您可以看到我添加了“pick”变量的使用,并且由于该等式出现在不同的上下文中,我不得不在实际的随机选择周围添加 $(( )) 表示法。

但是,这段代码中有一个错误。你能发现吗?这实际上是程序员常犯的经典错误。

问题是什么?until 循环假设 $hand[n] 的值为 -1,并且保持不变,直到从牌堆中随机抽出的有效牌被分配给它。但是,数组元素的值在首次分配时是未定义的——这不好。

相反,在此片段上方需要快速初始化


# start with an undealt hand:
for card in {0..5} ; do
  hand[$card]=-1
done

我们几乎真的准备好发一手牌,看看我们得到了什么。但是,在我们这样做之前,还有一项任务:一个可以将像 21 这样的数值转换为可读的牌值的例程,例如“方块九”或更简洁的“9D”。

每种花色有四种,每种花色有 13 种可能的牌值,这意味着需要 div 和 mod 函数:rank = card % 13suit = card / 13

我们需要一种将花色映射到其助记符的方法:红桃、梅花、方块和黑桃。使用另一个数组很容易


suits[0]="H"; suits[1]="C"; suits[2]="D"; suits[3]="S";

初始化后,显示给定牌的有意义的值非常简单


showcard()
{
  suit=$(( $1 / 13 ))
  rank=$(( ( $1 % 13 ) + 1 ))
  showcardvalue=$rank${suits[$suit]}
}

实际上,这不太正确,因为我们不希望得到像 11H 或 1D 这样的结果;我们希望将 1 转换为 A,将 11 转换为 J,等等。这是 case 语句的完美用法


case $rank in
  1)  rank="A" ;;
  11) rank="J" ;;
  12) rank="Q" ;;
  13) rank="K" ;;
esac

现在我们准备好发一手牌,看看我们得到了什么


for card in {0..5} ; do
  until [ ${hand[$card]} -ne -1 ]
  do
   pick=$(( $RANDOM % 52 ))
    hand[$card]=${deck[$pick]}
  done
  showcard ${hand[$card]} # sets 'showcardvalue'
  echo "Card ${card}: $showcardvalue"
  deck[$pick]=-1     # no longer available
done

运行结果如何?以下是一些迭代


$ sh cribbage.sh
Card 0: 5D
Card 1: 5C
Card 2: JS
Card 3: QD
Card 4: 4D
Card 5: JD
$ sh cribbage.sh
Card 0: 10C
Card 1: 5D
Card 2: KC
Card 3: 7S
Card 4: 4S
Card 5: 8C

酷。现在我们掌握了如何模拟牌堆和发一手独特的牌的基础知识,我们可以从有趣的元素开始——下个月。同时,您的家庭作业是学习二十一点

二十一点照片,来自 Shutterstock.com。

Dave Taylor 长期以来一直在 UNIX 和 Linux 系统上破解 shell 脚本。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上找到他,用户名是 @DaveTaylor,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论