问题
是否有最好的价值让我赢得尽可能多的比赛?如果是这样,它是什么?
编辑:对于给定的限制,是否存在可以计算出的确切获胜概率,而与对手的行为无关?(自大学以来我没有做过概率和统计)。我有兴趣将其视为与我的模拟结果进行对比的答案。
编辑:修复了我的算法中的错误,更新了结果表。
背景
我一直在玩一个修改后的二十一点游戏,其中一些相当烦人的规则调整来自标准规则。我将与标准二十一点规则不同的规则用斜体表示,并为不熟悉的人添加了二十一点规则。
修改后的二十一点规则
- 正好是两个人类玩家(庄家无关)
- 每个玩家发两张牌面朝下
- 玩家_ever_都不知道对手的_any_张牌的价值
- 在_双方_都完成手牌之前,两位玩家都不知道对手手牌的价值
- 目标是尽可能接近 21 分。结果:
- 如果玩家的 A 和 B 得分相同,则游戏为平局
- 如果玩家的 A 和 B 的分数都超过 21(一个失败),则游戏为平局
- 如果玩家 A 的分数 <= 21 并且玩家 B 已出局,则玩家 A获胜
- 如果玩家 A 的分数大于玩家 B 的分数,并且两者都没有破坏,则玩家 A获胜
- 否则,玩家 A 输了(B 赢了)。
- 卡片价值:
- 卡片 2 到 10 值相应数量的积分
- J、Q、K 牌值 10 分
- 王牌牌值 1 或 11 点
- 每个玩家可以一次申请一张额外的牌,直到:
- 玩家不再想要(留下)
- 玩家的得分,任何 A 计为 1,超过 21(失败)
- 双方玩家都不知道对方在任何时候使用了多少张牌
- 一旦两名玩家都留下或出局,则根据上述规则 3 确定获胜者。
- 每手牌后,整副牌重新洗牌,所有 52 张牌再次出局
什么是一副纸牌?
一副牌由 52 张牌组成,以下 13 个值各有 4 张:
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
用法
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,所以我应该扔掉那些反对者限制值吗?另外,我如何修改它以考虑到真实人类对手的可变性?例如,一个真正的人可能仅基于“感觉”而停留在 15 上,也可能基于“感觉”而击中 18