问题
有没有一个最好的点数可以停留,使我赢得可能的最大比例?如果有,那是多少?
编辑:是否存在一个确切的胜率概率,可以独立于对手所做的任何事情计算出给定限制的胜率?(我自大学以来就没有学过概率与统计学)。我很想看到它作为答案,以便与我的模拟结果进行对比。
编辑:修复了算法中的错误,更新了结果表格。
背景
我一直在玩一个修改过的二十一点游戏,其中有一些非常恼人的规则调整不同于标准规则。我已经用斜体标出与标准二十一点规则不同的规则,并包括对于不熟悉规则的人的二十一点规则。
修改后的二十一点规则
- 正好两个人类玩家(庄家无关紧要)
- 每个玩家都会被发两张脸朝下的牌
- 没有一个玩家知道任何对手牌的价值
- 除非两个玩家都完成了这手牌,否则没有一个玩家知道对手的手牌价值
- 目标是尽可能接近21分。结果:
- 如果玩家A和B的得分相同,则游戏为平局
- 如果玩家A和B都有超过21分的得分(爆牌),则游戏为平局
- 如果玩家A的得分小于等于21且玩家B已经爆牌,则玩家A获胜
- 如果玩家A的得分大于玩家B的得分,并且两者都没有爆牌,则玩家A获胜
- 否则,玩家A输(B赢)。
- 卡牌价值如下:
- 2到10的牌面点数与其相应的点数相同
- J、Q、K的牌面点数为10点
- Ace牌为1或11点
- 每个玩家可以一次请求一张附加卡牌,直到:
- 玩家不想再要了(停留)
- 玩家的得分,将任何Ace计为1,超过21(爆牌)
- 任何时候,没有一个玩家知道对手使用了多少张牌
- 一旦两个玩家都停留或爆牌,根据规则3确定赢家。
- 每次发完牌后整个牌堆都会重新洗牌,所有52张牌都会再次使用
什么是一副牌?
一副牌由52张牌组成,每种牌面点数有四张牌:
"2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A"。没有其他卡牌属性是相关的。这个Ruby表示是:CARDS = ((2..11).to_a+[10]*3)*4
算法
我的方法如下:
- 如果我的得分在2到11之间,我总是希望要牌,因为不可能爆牌
- 对于每个得分从12到21,我将模拟N次与对手的比赛
- 对于这N次比赛,我的得分将是我的“限制”。一旦达到或超过限制,我将停止要牌。
- 我的对手将按照完全相同的策略行动
- 我将为(12..21)和(12..21)的每个组合模拟N次比赛
- 打印每个组合的胜负差以及净胜负差
这里是用Ruby实现的算法:
#!/usr/bin/env ruby
class Array
def shuffle
sort_by { rand }
end
def shuffle!
self.replace shuffle
end
def score
sort.each_with_index.inject(0){|s,(c,i)|
s+c > 21 - (size - (i + 1)) && c==11 ? s+1 : s+c
}
end
end
N=(ARGV[0]||100_000).to_i
NDECKS = (ARGV[1]||1).to_i
CARDS = ((2..11).to_a+[10]*3)*4*NDECKS
CARDS.shuffle
my_limits = (12..21).to_a
opp_limits = my_limits.dup
puts " " * 55 + "opponent_limit"
printf "my_limit |"
opp_limits.each do |result|
printf "%10s", result.to_s
end
printf "%10s", "net"
puts
printf "-" * 8 + " |"
print " " + "-" * 8
opp_limits.each do |result|
print " " + "-" * 8
end
puts
win_totals = Array.new(10)
win_totals.map! { Array.new(10) }
my_limits.each do |my_limit|
printf "%8s |", my_limit
$stdout.flush
opp_limits.each do |opp_limit|
if my_limit == opp_limit # will be a tie, skip
win_totals[my_limit-12][opp_limit-12] = 0
print " --"
$stdout.flush
next
elsif win_totals[my_limit-12][opp_limit-12] # if previously calculated, print
printf "%10d", win_totals[my_limit-12][opp_limit-12]
$stdout.flush
next
end
win = 0
lose = 0
draw = 0
N.times {
cards = CARDS.dup.shuffle
my_hand = [cards.pop, cards.pop]
opp_hand = [cards.pop, cards.pop]
# hit until I hit limit
while my_hand.score < my_limit
my_hand << cards.pop
end
# hit until opponent hits limit
while opp_hand.score < opp_limit
opp_hand << cards.pop
end
my_score = my_hand.score
opp_score = opp_hand.score
my_score = 0 if my_score > 21
opp_score = 0 if opp_score > 21
if my_hand.score == opp_hand.score
draw += 1
elsif my_score > opp_score
win += 1
else
lose += 1
end
}
win_totals[my_limit-12][opp_limit-12] = win-lose
win_totals[opp_limit-12][my_limit-12] = lose-win # shortcut for the inverse
printf "%10d", win-lose
$stdout.flush
end
printf "%10d", win_totals[my_limit-12].inject(:+)
puts
end
Usage
ruby blackjack.rb [num_iterations] [num_decks]
这个脚本默认执行100,000次迭代和4个堆。在快速的MacBook Pro上,100,000次迭代需要大约5分钟。
输出结果(N = 100,000)
opponent_limit
my_limit | 12 13 14 15 16 17 18 19 20 21 net
-------- | -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- --------
12 | -- -7666 -13315 -15799 -15586 -10445 -2299 12176 30365 65631 43062
13 | 7666 -- -6962 -11015 -11350 -8925 -975 10111 27924 60037 66511
14 | 13315 6962 -- -6505 -9210 -7364 -2541 8862 23909 54596 82024
15 | 15799 11015 6505 -- -5666 -6849 -4281 4899 17798 45773 84993
16 | 15586 11350 9210 5666 -- -6149 -5207 546 11294 35196 77492
17 | 10445 8925 7364 6849 6149 -- -7790 -5317 2576 23443 52644
18 | 2299 975 2541 4281 5207 7790 -- -11848 -7123 8238 12360
19 | -12176 -10111 -8862 -4899 -546 5317 11848 -- -18848 -8413 -46690
20 | -30365 -27924 -23909 -17798 -11294 -2576 7123 18848 -- -28631 -116526
21 | -65631 -60037 -54596 -45773 -35196 -23443 -8238 8413 28631 -- -255870
翻译
这是我困惑的地方。我不太确定如何解释这些数据。乍一看,似乎总是停留在16或17是最好的选择,但我不确定是否那么简单。我认为实际的人类对手不太可能停留在12、13和可能的14,所以我应该排除这些opponent_limit值吗?另外,我该如何修改这个模型来考虑真实人类对手的可变性?例如,一个真正的人类对手可能会基于“感觉”停留在15上,也可能会基于“感觉”在18上击中。