9

问题

是否有最好的价值让我赢得尽可能多的比赛?如果是这样,它是什么?

编辑:对于给定的限制,是否存在可以计算出的确切获胜概率,而与对手的行为无关?(自大学以来我没有做过概率和统计)。我有兴趣将其视为与我的模拟结果进行对比的答案。

编辑:修复了我的算法中的错误,更新了结果表。

背景

我一直在玩一个修改后的二十一点游戏,其中一些相当烦人的规则调整来自标准规则。我将与标准二十一点规则不同的规则用斜体表示,并为不熟悉的人添加了二十一点规则。

修改后的二十一点规则

  1. 正好是两个人类玩家(庄家无关)
  2. 每个玩家发两张牌面朝下
    • 玩家_ever_都不知道对手的_any_张牌的价值
    • 在_双方_都完成手牌之前,两位玩家都不知道对手手牌的价值
  3. 目标是尽可能接近 21 分。结果:
    • 如果玩家的 A 和 B 得分相同,则游戏为平局
    • 如果玩家的 A 和 B 的分数都超过 21(一个失败),则游戏为平局
    • 如果玩家 A 的分数 <= 21 并且玩家 B 已出局,则玩家 A获胜
    • 如果玩家 A 的分数大于玩家 B 的分数,并且两者都没有破坏,则玩家 A获胜
    • 否则,玩家 A 输了(B 赢了)。
  4. 卡片价值:
    • 卡片 2 到 10 值相应数量的积分
    • J、Q、K 牌值 10 分
    • 王牌牌值 1 或 11 点
  5. 每个玩家可以一次申请一张额外的牌,直到:
    • 玩家不再想要(留下)
    • 玩家的得分,任何 A 计为 1,超过 21(失败)
    • 双方玩家都不知道对方在任何时候使用了多少张牌
  6. 一旦两名玩家都留下或出局,则根据上述规则 3 确定获胜者。
  7. 每手牌后,整副牌重新洗牌,所有 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

4

3 回答 3

4

我怀疑你的结果。例如,如果对手的目标是 19,那么您的数据表明,击败他的最佳方法是一直打到 20。这没有通过基本的嗅觉测试。你确定你没有bug?如果我的对手争取 19 或更高,我的策略将是不惜一切代价避免破坏:坚持任何 13 或更高(甚至可能是 12?)。争取 20 分肯定是错误的——不仅是小幅度的,而且是很多的。

我怎么知道你的数据是坏的?因为你玩的二十一点游戏并不稀奇。这是庄家在大多数赌场中的游戏方式:庄家击中目标然后停止,不管其他玩家手里拿着什么。那个目标是什么?站在硬 17 上,打软 17。当你摆脱脚本中的错误时,它应该确认赌场知道他们的业务。

当我对您的代码进行以下替换时:

# Replace scoring method.
def score
  s = inject(0) { |sum, c| sum + c }
  return s if s < 21
  n_aces = find_all { |c| c == 11 }.size
  while s > 21 and n_aces > 0
      s -= 10
      n_aces -= 1
  end
  return s
end

# Replace section of code determining hand outcome.
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_score == opp_score
  draw += 1
elsif my_score > opp_score
  win += 1
else
  lose += 1
end

结果与赌场荷官的行为一致:17 是最优目标

n=10000
                                                       opponent_limit
my_limit |        12        13        14        15        16        17        18        19        20        21       net
-------- |  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------
      12 |        --      -843     -1271     -1380     -1503     -1148      -137      1234      3113      6572
      13 |       843        --      -642     -1041     -1141      -770       -93      1137      2933      6324
      14 |      1271       642        --      -498      -784      -662        93      1097      2977      5945
      15 |      1380      1041       498        --      -454      -242      -100       898      2573      5424
      16 |      1503      1141       784       454        --      -174        69       928      2146      4895
      17 |      1148       770       662       242       174        --        38       631      1920      4404
      18 |       137        93       -93       100       -69       -38        --       489      1344      3650
      19 |     -1234     -1137     -1097      -898      -928      -631      -489        --       735      2560
      20 |     -3113     -2933     -2977     -2573     -2146     -1920     -1344      -735        --      1443
      21 |     -6572     -6324     -5945     -5424     -4895     -4404     -3650     -2560     -1443        --

一些杂项评论

当前的设计是不灵活的。只需进行一点重构,您就可以在游戏操作(发牌、洗牌、保持运行统计数据)和玩家决策之间实现清晰的分离。这将允许您相互测试各种策略。目前,您的策略嵌入在循环中,这些循环都纠缠在游戏操作代码中。允许您创建新玩家并随意设置他们的策略的设计会更好地为您的实验提供服务。

于 2010-02-20T15:37:07.193 回答
2

两条评论:

  1. 看起来没有基于“命中限制”的单一主导策略:

    • 如果你选择 16 你的对手可以选择 17
    • 如果你选择 17 你的对手可以选择 18
    • 如果你选择 18 你的对手可以选择 19
    • 如果你选择 19 你的对手可以选择 20
    • 如果你选择 20 你的对手可以选择 12
    • 如果你选择 12,你的对手可以选择 16。

2.你没有提到玩家是否可以看到他们的对手抽了多少张牌(我猜是这样)。我希望将这些信息纳入“最佳”策略。(已回答)


由于没有关于其他玩家决定的信息,游戏变得更简单。但由于显然不存在占优势的“纯”策略,最优策略将是“混合”策略。也就是说:从 12 到 21 的每个分数的一组概率,用于判断您是否应该停止或绘制另一张牌(编辑:对于没有 A 的给定分数与有 A 的分数,您将需要不同的概率。)然后执行该策略需要您可以随机选择(根据概率)在每次新开奖后是停止还是继续。然后,您可以找到游戏的纳什均衡

当然,如果您只问一个更简单的问题:对抗次优玩家(例如总是停在 16、17、18 或 19 的玩家)的最佳获胜策略是什么,那么您问的是一个完全不同的问题,您将不得不准确指定与您相比,其他玩家受到限制的方式。

于 2010-02-20T09:10:58.277 回答
1

以下是关于您收集的数据的一些想法:

  • 告诉你你的“命中限制”应该是多少有点用处,但前提是你知道你的对手正在遵循类似的“命中限制”策略。
  • 即便如此,只有当您知道对手的“命中限制”是或可能是什么时,它才会真正有用。您可以选择一个让您获得比他们更多胜利的限制。
  • 您可以或多或少地忽略表中的实际值。重要的是它们是积极的还是消极的。

以另一种方式显示您的数据,第一个数字是您对手的限制,第二组数字是您可以选择并获胜的限制。带星号的是“最成功”的选择:

12:   13, 14, 15, 16*, 17, 18
13:   14, 15, 16*, 17, 18, 19
14:   15, 16, 17*, 18, 19
15:   16, 17*, 18, 19
16:   17, 18*, 19
17:   18*, 19
18:   19*, 20
19:   12, 20*
20:   12*, 13, 14, 15, 16, 17
21:   12*, 13, 14, 15, 16, 17, 18, 19, 20

从中可以看出,如果对手遵循随机的“命中限制”选择策略,17 或 18 的命中限制是最安全的选择,因为 17 和 18 将击败 7/10 的对手“命中限制”。

当然,如果你的对手是人类,你就不能对他们进行自我施加低于 18 或超过 19 的“命中限制”,这样就完全否定了之前的计算。我仍然认为这些数字很有用:


我同意,对于任何一手牌,你都可以有理由相信你的对手会有一个限制,在这个限制之后他们将停止击球,并且他们会留下来。如果您可以猜测该限制,则可以根据该估计选择自己的限制。

如果您认为他们很乐观或乐于冒险,请选择 20 的限制 - 从长远来看,只要他们的限制高于 17,您就会击败他们。如果您真的有信心,请选择 20 的限制12 - 如果他们的限制高于 18 则将获胜,并且这里有更频繁的奖金。

如果您认为他们保守或厌恶风险,请选择 18 的限制。如果他们自己保持在 18 岁以下,那将获胜。

对于中立地,也许考虑一下在没有任何外部影响的情况下您的极限是多少。你通常会打16吗?一个17?

简而言之,你只能猜测对手的极限是多少,但如果你猜对了,你可以用这些数据长期击败他们。

于 2010-02-20T08:52:42.060 回答